From 539dcacd0076951eb6d64d4f5b5f9b84669002ad Mon Sep 17 00:00:00 2001 From: Vitalii Mikhailov Date: Fri, 6 Oct 2023 11:57:54 +0300 Subject: [PATCH] Added NexusModsModToModuleInfoHistoryEntity that will store the whole ModuleInfo entry of NexusMods mods Will allow us to check for mod updates Also, batched savings for Artcle and File parsers --- .../BUTR.Site.NexusMods.Server.csproj | 3 + .../Contexts/BaseAppDbContext.cs | 2 + ...dToModuleInfoHistoryEntityConfiguration.cs | 38 + .../Contexts/IAppDbContextRead.cs | 1 + .../Controllers/NexusModsUserController.cs | 10 +- .../Jobs/NexusModsArticleProcessorJob.cs | 98 +- .../NexusModsArticleUpdatesProcessorJob.cs | 11 +- .../Jobs/NexusModsModFileProcessorJob.cs | 92 +- .../NexusModsModFileUpdatesProcessorJob.cs | 18 +- ...exusModsModToModuleInfoHistory.Designer.cs | 1445 +++++++++++++++++ ...6075646_NexusModsModToModuleInfoHistory.cs | 66 + .../BaseAppDbContextModelSnapshot.cs | 57 +- .../Json/ApplicationVersionRangeModel.cs | 7 + .../Database/Json/DependentModuleModel.cs | 10 + .../Models/Database/Json/ModuleInfoModel.cs | 57 + .../Database/Json/SubModuleInfoModel.cs | 10 + .../Database/Json/SubModuleInfoTagModel.cs | 7 + .../NexusModsModToModuleInfoHistoryEntity.cs | 16 + ...sModsInfo.cs => NexusModsModFileParser.cs} | 87 +- src/BUTR.Site.NexusMods.Server/Startup.cs | 2 +- 20 files changed, 1886 insertions(+), 151 deletions(-) create mode 100644 src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToModuleInfoHistoryEntityConfiguration.cs create mode 100644 src/BUTR.Site.NexusMods.Server/Migrations/20231006075646_NexusModsModToModuleInfoHistory.Designer.cs create mode 100644 src/BUTR.Site.NexusMods.Server/Migrations/20231006075646_NexusModsModToModuleInfoHistory.cs create mode 100644 src/BUTR.Site.NexusMods.Server/Models/Database/Json/ApplicationVersionRangeModel.cs create mode 100644 src/BUTR.Site.NexusMods.Server/Models/Database/Json/DependentModuleModel.cs create mode 100644 src/BUTR.Site.NexusMods.Server/Models/Database/Json/ModuleInfoModel.cs create mode 100644 src/BUTR.Site.NexusMods.Server/Models/Database/Json/SubModuleInfoModel.cs create mode 100644 src/BUTR.Site.NexusMods.Server/Models/Database/Json/SubModuleInfoTagModel.cs create mode 100644 src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleInfoHistoryEntity.cs rename src/BUTR.Site.NexusMods.Server/Services/{NexusModsInfo.cs => NexusModsModFileParser.cs} (64%) diff --git a/src/BUTR.Site.NexusMods.Server/BUTR.Site.NexusMods.Server.csproj b/src/BUTR.Site.NexusMods.Server/BUTR.Site.NexusMods.Server.csproj index 4bf75a14..87304253 100644 --- a/src/BUTR.Site.NexusMods.Server/BUTR.Site.NexusMods.Server.csproj +++ b/src/BUTR.Site.NexusMods.Server/BUTR.Site.NexusMods.Server.csproj @@ -9,10 +9,13 @@ true $(NoWarn);1591 + + $(DefineConstants);BANNERLORDBUTRMODULEMANAGER_NULLABLE + diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/BaseAppDbContext.cs b/src/BUTR.Site.NexusMods.Server/Contexts/BaseAppDbContext.cs index cd4ee52e..2de6aacf 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/BaseAppDbContext.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/BaseAppDbContext.cs @@ -56,6 +56,7 @@ public class BaseAppDbContext : DbContext public required DbSet NexusModsModName { get; set; } public required DbSet NexusModsModModules { get; set; } public required DbSet NexusModsModToFileUpdates { get; set; } + public required DbSet NexusModsModToModuleInfoHistory { get; set; } public required DbSet StatisticsTopExceptionsTypes { get; set; } public required DbSet StatisticsCrashScoreInvolveds { get; set; } @@ -105,6 +106,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) _entityConfigurationFactory.ApplyConfigurationWithTenant(modelBuilder); _entityConfigurationFactory.ApplyConfigurationWithTenant(modelBuilder); _entityConfigurationFactory.ApplyConfigurationWithTenant(modelBuilder); + _entityConfigurationFactory.ApplyConfigurationWithTenant(modelBuilder); _entityConfigurationFactory.ApplyConfiguration(modelBuilder); _entityConfigurationFactory.ApplyConfigurationWithTenant(modelBuilder); diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToModuleInfoHistoryEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToModuleInfoHistoryEntityConfiguration.cs new file mode 100644 index 00000000..9eadba91 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToModuleInfoHistoryEntityConfiguration.cs @@ -0,0 +1,38 @@ +using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BUTR.Site.NexusMods.Server.Contexts.Configs; + +public class NexusModsModToModuleInfoHistoryEntityConfiguration : BaseEntityConfigurationWithTenant +{ + public NexusModsModToModuleInfoHistoryEntityConfiguration(ITenantContextAccessor tenantContextAccessor) : base(tenantContextAccessor) { } + + protected override void ConfigureModel(EntityTypeBuilder builder) + { + builder.Property(nameof(NexusModsModEntity.NexusModsModId)).HasColumnName("nexusmods_mod_name_id").HasConversion().ValueGeneratedNever(); + builder.Property(nameof(ModuleEntity.ModuleId)).HasColumnName("module_id").HasConversion(); + builder.Property(x => x.ModuleVersion).HasColumnName("module_version").HasConversion(); + builder.Property(x => x.ModuleInfo).HasColumnName("module_info").HasColumnType("jsonb"); + builder.ToTable("nexusmods_mod_module_info_history", "nexusmods_mod").HasKey(nameof(NexusModsModToModuleInfoHistoryEntity.TenantId), nameof(NexusModsModEntity.NexusModsModId), nameof(ModuleEntity.ModuleId), nameof(NexusModsModToModuleInfoHistoryEntity.ModuleVersion)); + + builder.HasOne(x => x.NexusModsMod) + .WithMany() + .HasForeignKey(nameof(NexusModsModToModuleInfoHistoryEntity.TenantId), nameof(NexusModsModEntity.NexusModsModId)) + .HasPrincipalKey(x => new { x.TenantId, x.NexusModsModId }) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(x => x.Module) + .WithMany() + .HasForeignKey(nameof(NexusModsModToModuleInfoHistoryEntity.TenantId), nameof(ModuleEntity.ModuleId)) + .HasPrincipalKey(x => new { x.TenantId, x.ModuleId }) + .OnDelete(DeleteBehavior.Cascade); + + builder.Navigation(x => x.NexusModsMod).AutoInclude(); + builder.Navigation(x => x.Module).AutoInclude(); + + base.ConfigureModel(builder); + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextRead.cs b/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextRead.cs index 6cf60aad..a255c474 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextRead.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextRead.cs @@ -42,6 +42,7 @@ public interface IAppDbContextRead DbSet NexusModsModName { get; } DbSet NexusModsModModules { get; } DbSet NexusModsModToFileUpdates { get; } + DbSet NexusModsModToModuleInfoHistory { get; } DbSet StatisticsTopExceptionsTypes { get; } DbSet StatisticsCrashScoreInvolveds { get; } diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsUserController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsUserController.cs index bf8c9b56..fe2244cf 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsUserController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsUserController.cs @@ -40,15 +40,15 @@ public sealed record NexusModsUserToNexusModsModManualLinkModel(NexusModsModId N private readonly ILogger _logger; private readonly NexusModsAPIClient _nexusModsAPIClient; - private readonly NexusModsInfo _nexusModsInfo; + private readonly NexusModsModFileParser _nexusModsModFileParser; private readonly IAppDbContextWrite _dbContextWrite; private readonly IAppDbContextRead _dbContextRead; - public NexusModsUserController(ILogger logger, NexusModsAPIClient nexusModsAPIClient, NexusModsInfo nexusModsInfo, IAppDbContextWrite dbContextWrite, IAppDbContextRead dbContextRead) + public NexusModsUserController(ILogger logger, NexusModsAPIClient nexusModsAPIClient, NexusModsModFileParser nexusModsModFileParser, IAppDbContextWrite dbContextWrite, IAppDbContextRead dbContextRead) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _nexusModsAPIClient = nexusModsAPIClient ?? throw new ArgumentNullException(nameof(nexusModsAPIClient)); - _nexusModsInfo = nexusModsInfo ?? throw new ArgumentNullException(nameof(nexusModsInfo)); + _nexusModsModFileParser = nexusModsModFileParser ?? throw new ArgumentNullException(nameof(nexusModsModFileParser)); _dbContextWrite = dbContextWrite ?? throw new ArgumentNullException(nameof(dbContextWrite)); _dbContextRead = dbContextRead ?? throw new ArgumentNullException(nameof(dbContextRead)); } @@ -178,12 +178,12 @@ public NexusModsUserController(ILogger logger, NexusMod if (HttpContext.GetIsPremium()) // Premium is needed for API based downloading { - var exposedModIds = await _nexusModsInfo.GetModIdsAsync(gameDomain, modInfo.Id, apiKey, ct).Distinct().ToImmutableArrayAsync(ct); + var exposedModIds = await _nexusModsModFileParser.GetModuleInfosAsync(gameDomain, modInfo.Id, apiKey, ct).Distinct().ToImmutableArrayAsync(ct); var entities = exposedModIds.Select(y => new NexusModsModToModuleEntity { TenantId = tenant, NexusModsMod = entityFactory.GetOrCreateNexusModsMod(query.NexusModsModId), - Module = entityFactory.GetOrCreateModule(y), + Module = entityFactory.GetOrCreateModule(ModuleId.From(y.Id)), LinkType = NexusModsModToModuleLinkType.ByUnverifiedFileExposure, LastUpdateDate = DateTime.UtcNow }).ToList(); diff --git a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsArticleProcessorJob.cs b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsArticleProcessorJob.cs index 61df1359..25bfc859 100644 --- a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsArticleProcessorJob.cs +++ b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsArticleProcessorJob.cs @@ -54,64 +54,70 @@ private static async Task HandleTenantAsync(TenantId tenant, IServiceProvider se var client = serviceProvider.GetRequiredService(); var dbContextWrite = serviceProvider.GetRequiredService(); var entityFactory = dbContextWrite.CreateEntityFactory(); - await using var _ = dbContextWrite.CreateSaveScope(); var gameDomain = tenant.ToGameDomain(); - var articles = new List(); - var articleIdRaw = 0; var notFoundArticles = 0; - while (!ct.IsCancellationRequested) + var @break = false; + while (!ct.IsCancellationRequested && !@break) { - var articleId = NexusModsArticleId.From(articleIdRaw); + await using var _ = dbContextWrite.CreateSaveScope(); + var articles = new List(); - var articleDocument = await client.GetArticleAsync(gameDomain, articleId, ct); - if (articleDocument is null) continue; + for (var i = 0; i < 50; i++) + { + var articleId = NexusModsArticleId.From(articleIdRaw); + if (await client.GetArticleAsync(gameDomain, articleId, ct) is not { } articleDocument) + { + articleIdRaw++; + continue; + } - var errorElement = articleDocument.GetElementbyId($"{articleId}-title"); - if (errorElement is not null) - { - notFoundArticles++; - articleIdRaw++; - if (notFoundArticles >= notFoundArticlesTreshold) + if (articleDocument.GetElementbyId($"{articleId}-title") is not null) { - break; + notFoundArticles++; + articleIdRaw++; + if (notFoundArticles >= notFoundArticlesTreshold) + { + @break = true; + break; + } + continue; } - continue; + notFoundArticles = 0; + + var pagetitleElement = articleDocument.GetElementbyId("pagetitle"); + var titleElement = pagetitleElement.ChildNodes.FindFirst("h1"); + var title = titleElement.InnerText; + + var authorElement = articleDocument.GetElementbyId("image-author-name"); + var authorUrl = authorElement.GetAttributeValue("href", "0"); + var authorUrlSplit = authorUrl.Split('/', StringSplitOptions.RemoveEmptyEntries); + var authorIdText = authorUrlSplit.LastOrDefault() ?? string.Empty; + var authorId = NexusModsUserId.TryParse(authorIdText, out var val) ? val : throw new Exception("Author Id invalid"); + var authorText = authorElement.InnerText; + + var fileinfoElement = articleDocument.GetElementbyId("fileinfo"); + var dateTimeText1 = fileinfoElement.ChildNodes.FindFirst("div"); + var dateTimeText2 = dateTimeText1?.ChildNodes.FindFirst("time"); + var dateTimeText = dateTimeText2?.GetAttributeValue("datetime", ""); + var dateTime = DateTimeOffset.TryParse(dateTimeText, out var dateTimeVal) ? dateTimeVal.UtcDateTime : DateTimeOffset.MinValue.UtcDateTime; + + articles.Add(new() + { + TenantId = tenant, + Title = title, + NexusModsArticleId = articleId, + NexusModsUser = entityFactory.GetOrCreateNexusModsUserWithName(authorId, NexusModsUserName.From(authorText)), + CreateDate = dateTime + }); + articleIdRaw++; } - notFoundArticles = 0; - - var pagetitleElement = articleDocument.GetElementbyId("pagetitle"); - var titleElement = pagetitleElement.ChildNodes.FindFirst("h1"); - var title = titleElement.InnerText; - - var authorElement = articleDocument.GetElementbyId("image-author-name"); - var authorUrl = authorElement.GetAttributeValue("href", "0"); - var authorUrlSplit = authorUrl.Split('/', StringSplitOptions.RemoveEmptyEntries); - var authorIdText = authorUrlSplit.LastOrDefault() ?? string.Empty; - var authorId = NexusModsUserId.TryParse(authorIdText, out var val) ? val : throw new Exception("Author Id invalid"); - var authorText = authorElement.InnerText; - - var fileinfoElement = articleDocument.GetElementbyId("fileinfo"); - var dateTimeText1 = fileinfoElement.ChildNodes.FindFirst("div"); - var dateTimeText2 = dateTimeText1?.ChildNodes.FindFirst("time"); - var dateTimeText = dateTimeText2?.GetAttributeValue("datetime", ""); - var dateTime = DateTimeOffset.TryParse(dateTimeText, out var dateTimeVal) ? dateTimeVal.UtcDateTime : DateTimeOffset.MinValue.UtcDateTime; - - articles.Add(new() - { - TenantId = tenant, - Title = title, - NexusModsArticleId = articleId, - NexusModsUser = entityFactory.GetOrCreateNexusModsUserWithName(authorId, NexusModsUserName.From(authorText)), - CreateDate = dateTime - }); - articleIdRaw++; - } - dbContextWrite.FutureUpsert(x => x.NexusModsArticles, articles); - // Disposing the DBContext will save the data + dbContextWrite.FutureUpsert(x => x.NexusModsArticles, articles); + // Disposing the DBContext will save the data + } } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsArticleUpdatesProcessorJob.cs b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsArticleUpdatesProcessorJob.cs index a5b874a0..fca563ab 100644 --- a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsArticleUpdatesProcessorJob.cs +++ b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsArticleUpdatesProcessorJob.cs @@ -69,12 +69,13 @@ private static async Task HandleTenantAsync(TenantId tenant, IServiceProvid { var articleId = NexusModsArticleId.From(articleIdRaw); - var articleDocument = await client.GetArticleAsync(gameDomain, articleId, ct); - if (articleDocument is null) continue; - + if (await client.GetArticleAsync(gameDomain, articleId, ct) is not { } articleDocument) + { + articleIdRaw++; + continue; + } - var errorElement = articleDocument.GetElementbyId($"{articleId}-title"); - if (errorElement is not null) + if (articleDocument.GetElementbyId($"{articleId}-title") is not null) { notFoundArticles++; articleIdRaw++; diff --git a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileProcessorJob.cs b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileProcessorJob.cs index 6fcd7c6c..93129a9b 100644 --- a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileProcessorJob.cs +++ b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileProcessorJob.cs @@ -57,70 +57,84 @@ private static async Task HandleTenantAsync(TenantId tenant, IServiceProvider se { const int notFoundModIdsTreshold = 25; - var info = serviceProvider.GetRequiredService(); + var info = serviceProvider.GetRequiredService(); var options = serviceProvider.GetRequiredService>().Value; var client = serviceProvider.GetRequiredService(); var dbContextRead = serviceProvider.GetRequiredService(); var dbContextWrite = serviceProvider.GetRequiredService(); var entityFactory = dbContextWrite.CreateEntityFactory(); - await using var _ = dbContextWrite.CreateSaveScope(); var gameDomain = tenant.ToGameDomain(); var modIdRaw = 0; var notFoundModIds = 0; - while (!ct.IsCancellationRequested) + var @break = false; + while (!ct.IsCancellationRequested && !@break) { - var modId = NexusModsModId.From(modIdRaw); - + await using var _ = dbContextWrite.CreateSaveScope(); var nexusModsModModuleEntities = new List(); var nexusModsModToFileUpdateEntities = new List(); - - var updateDate = await dbContextRead.NexusModsModToFileUpdates.FirstOrDefaultAsync(x => x.NexusModsMod.NexusModsModId == modId, ct); - var files = await client.GetModFileInfosAsync(gameDomain, modId, options.ApiKey, ct); - if (files is null) + var nexusModsModToModuleInfoHistoryEntities = new List(); + + for (var i = 0; i < 50; i++) { - notFoundModIds++; - modIdRaw++; - if (notFoundModIds >= notFoundModIdsTreshold) + var modId = NexusModsModId.From(modIdRaw); + + var updateDate = await dbContextRead.NexusModsModToFileUpdates.FirstOrDefaultAsync(x => x.NexusModsMod.NexusModsModId == modId, ct); + if (await client.GetModFileInfosAsync(gameDomain, modId, options.ApiKey, ct) is not { } files) { - break; + notFoundModIds++; + modIdRaw++; + if (notFoundModIds >= notFoundModIdsTreshold) + { + @break = true; + break; + } + continue; } - continue; - } - notFoundModIds = 0; + notFoundModIds = 0; - // max sequence no elements - var latestFileUpdate = files.Files.Select(x => DateTimeOffset.FromUnixTimeSeconds(x.UploadedTimestamp).UtcDateTime).DefaultIfEmpty(DateTime.MinValue).Max(); - if (latestFileUpdate != DateTime.MinValue) - { - if (updateDate is null || updateDate.LastCheckedDate < latestFileUpdate) + // max sequence no elements + var latestFileUpdate = files.Files.Select(x => DateTimeOffset.FromUnixTimeSeconds(x.UploadedTimestamp).UtcDateTime).DefaultIfEmpty(DateTime.MinValue).Max(); + if (latestFileUpdate != DateTime.MinValue) { - var exposedModIds = await info.GetModIdsAsync(gameDomain, modId, options.ApiKey, ct).Distinct().ToImmutableArrayAsync(ct); - - var id = modIdRaw; - nexusModsModModuleEntities.AddRange(exposedModIds.Select(x => new NexusModsModToModuleEntity - { - TenantId = tenant, - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(NexusModsModId.From(id)), - Module = entityFactory.GetOrCreateModule(x), - LastUpdateDate = latestFileUpdate, - LinkType = NexusModsModToModuleLinkType.ByUnverifiedFileExposure, - })); - nexusModsModToFileUpdateEntities.Add(new NexusModsModToFileUpdateEntity + //if (updateDate is null || updateDate.LastCheckedDate < latestFileUpdate) { - TenantId = tenant, - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(modId), - LastCheckedDate = latestFileUpdate - }); + var exposedModuleInfos = await info.GetModuleInfosAsync(gameDomain, modId, options.ApiKey, ct).ToArrayAsync(ct); + + var id = modIdRaw; + nexusModsModModuleEntities.AddRange(exposedModuleInfos.DistinctBy(x => x.Id).Select(x => new NexusModsModToModuleEntity + { + TenantId = tenant, + NexusModsMod = entityFactory.GetOrCreateNexusModsMod(NexusModsModId.From(id)), + Module = entityFactory.GetOrCreateModule(ModuleId.From(x.Id)), + LastUpdateDate = latestFileUpdate, + LinkType = NexusModsModToModuleLinkType.ByUnverifiedFileExposure, + })); + nexusModsModToFileUpdateEntities.Add(new NexusModsModToFileUpdateEntity + { + TenantId = tenant, + NexusModsMod = entityFactory.GetOrCreateNexusModsMod(modId), + LastCheckedDate = latestFileUpdate, + }); + nexusModsModToModuleInfoHistoryEntities.AddRange(exposedModuleInfos.DistinctBy(x => new { x.Id, x.Version }).Select(x => new NexusModsModToModuleInfoHistoryEntity + { + TenantId = tenant, + NexusModsMod = entityFactory.GetOrCreateNexusModsMod(modId), + Module = entityFactory.GetOrCreateModule(ModuleId.From(x.Id)), + ModuleVersion = ModuleVersion.From(x.Version.ToString()), + ModuleInfo = ModuleInfoModel.Create(x), + })); + } } + + modIdRaw++; } dbContextWrite.FutureUpsert(x => x.NexusModsModModules, nexusModsModModuleEntities); dbContextWrite.FutureUpsert(x => x.NexusModsModToFileUpdates, nexusModsModToFileUpdateEntities); + dbContextWrite.FutureUpsert(x => x.NexusModsModToModuleInfoHistory, nexusModsModToModuleInfoHistoryEntities); // Disposing the DBContext will save the data - - modIdRaw++; } } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileUpdatesProcessorJob.cs b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileUpdatesProcessorJob.cs index 08d4b20b..6b2d1029 100644 --- a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileUpdatesProcessorJob.cs +++ b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileUpdatesProcessorJob.cs @@ -72,7 +72,7 @@ public async Task Execute(IJobExecutionContext context) { var gameDomain = tenant.ToGameDomain(); - var info = serviceProvider.GetRequiredService(); + var info = serviceProvider.GetRequiredService(); var options = serviceProvider.GetRequiredService>().Value; var client = serviceProvider.GetRequiredService(); var dbContextRead = serviceProvider.GetRequiredService(); @@ -95,20 +95,21 @@ public async Task Execute(IJobExecutionContext context) var exceptions = new List(); var nexusModsModModuleEntities = new List(); var nexusModsModToFileUpdateEntities = new List(); + var nexusModsModToModuleInfoHistoryEntities = new List(); foreach (var modUpdate in newUpdates) { try { if (ct.IsCancellationRequested) break; - var exposedModIds = await info.GetModIdsAsync(gameDomain, modUpdate.Id, options.ApiKey, ct).Distinct().ToImmutableArrayAsync(ct); + var exposedModuleInfos = await info.GetModuleInfosAsync(gameDomain, modUpdate.Id, options.ApiKey, ct).Distinct().ToArrayAsync(ct); var lastUpdateTime = DateTimeOffset.FromUnixTimeSeconds(modUpdate.LatestFileUpdateTimestamp).UtcDateTime; - nexusModsModModuleEntities.AddRange(exposedModIds.Select(x => new NexusModsModToModuleEntity + nexusModsModModuleEntities.AddRange(exposedModuleInfos.DistinctBy(x => x.Id).Select(x => new NexusModsModToModuleEntity { TenantId = tenant, NexusModsMod = entityFactory.GetOrCreateNexusModsMod(modUpdate.Id), - Module = entityFactory.GetOrCreateModule(x), + Module = entityFactory.GetOrCreateModule(ModuleId.From(x.Id)), LastUpdateDate = lastUpdateTime, LinkType = NexusModsModToModuleLinkType.ByUnverifiedFileExposure })); @@ -118,6 +119,14 @@ public async Task Execute(IJobExecutionContext context) NexusModsMod = entityFactory.GetOrCreateNexusModsMod(modUpdate.Id), LastCheckedDate = lastUpdateTime }); + nexusModsModToModuleInfoHistoryEntities.AddRange(exposedModuleInfos.DistinctBy(x => new { x.Id, x.Version }).Select(x => new NexusModsModToModuleInfoHistoryEntity + { + TenantId = tenant, + NexusModsMod = entityFactory.GetOrCreateNexusModsMod(modUpdate.Id), + Module = entityFactory.GetOrCreateModule(ModuleId.From(x.Id)), + ModuleVersion = ModuleVersion.From(x.Version.ToString()), + ModuleInfo = ModuleInfoModel.Create(x), + })); processed++; } catch (Exception e) @@ -128,6 +137,7 @@ public async Task Execute(IJobExecutionContext context) dbContextWrite.FutureUpsert(x => x.NexusModsModModules, nexusModsModModuleEntities); dbContextWrite.FutureUpsert(x => x.NexusModsModToFileUpdates, nexusModsModToFileUpdateEntities); + dbContextWrite.FutureUpsert(x => x.NexusModsModToModuleInfoHistory, nexusModsModToModuleInfoHistoryEntities); // Disposing the DBContext will save the data return (processed, exceptions, updatesStoredWithinDay.Count, updatedWithinDay.Length, newUpdates.Count); diff --git a/src/BUTR.Site.NexusMods.Server/Migrations/20231006075646_NexusModsModToModuleInfoHistory.Designer.cs b/src/BUTR.Site.NexusMods.Server/Migrations/20231006075646_NexusModsModToModuleInfoHistory.Designer.cs new file mode 100644 index 00000000..2489edb8 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Migrations/20231006075646_NexusModsModToModuleInfoHistory.Designer.cs @@ -0,0 +1,1445 @@ +// +using System; +using System.Collections.Generic; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BUTR.Site.NexusMods.Server.Migrations +{ + [DbContext(typeof(BaseAppDbContext))] + [Migration("20231006075646_NexusModsModToModuleInfoHistory")] + partial class NexusModsModToModuleInfoHistory + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0-rc.1.23419.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.AutocompleteEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("AutocompleteId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("autocomplete_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AutocompleteId")); + + b.Property("Type") + .IsRequired() + .HasColumnType("text") + .HasColumnName("type"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("TenantId", "AutocompleteId"); + + b.HasIndex("Type"); + + b.ToTable("autocomplete", "autocomplete"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.CrashReportEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("CrashReportId") + .HasColumnType("uuid") + .HasColumnName("crash_report_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Exception") + .IsRequired() + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("ExceptionTypeId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("exception_type_id"); + + b.Property("GameVersion") + .IsRequired() + .HasColumnType("text") + .HasColumnName("game_version"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text") + .HasColumnName("url"); + + b.Property("Version") + .HasColumnType("smallint") + .HasColumnName("version"); + + b.HasKey("TenantId", "CrashReportId"); + + b.HasIndex("TenantId", "ExceptionTypeId"); + + b.ToTable("crash_report", "crashreport"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.CrashReportIgnoredFileEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("crash_report_file_ignored_id"); + + b.HasKey("TenantId", "Value"); + + b.ToTable("crash_report_file_ignored", "crashreport"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.CrashReportToFileIdEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("CrashReportId") + .HasColumnType("uuid") + .HasColumnName("crash_report_file_id"); + + b.Property("FileId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("file_id"); + + b.HasKey("TenantId", "CrashReportId"); + + b.ToTable("crash_report_file", "crashreport"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.CrashReportToMetadataEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("CrashReportId") + .HasColumnType("uuid") + .HasColumnName("crash_report_metadata_id"); + + b.Property("BLSEVersion") + .HasColumnType("text") + .HasColumnName("blse_version"); + + b.Property("BUTRLoaderVersion") + .HasColumnType("text") + .HasColumnName("butrloader_version"); + + b.Property("LauncherExVersion") + .HasColumnType("text") + .HasColumnName("launcherex_version"); + + b.Property("LauncherType") + .HasColumnType("text") + .HasColumnName("launcher_type"); + + b.Property("LauncherVersion") + .HasColumnType("text") + .HasColumnName("launcher_version"); + + b.Property("Runtime") + .HasColumnType("text") + .HasColumnName("runtime"); + + b.HasKey("TenantId", "CrashReportId"); + + b.ToTable("crash_report_metadata", "crashreport"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.CrashReportToModuleMetadataEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("CrashReportId") + .HasColumnType("uuid") + .HasColumnName("crash_report_module_info_id"); + + b.Property("ModuleId") + .HasColumnType("text") + .HasColumnName("module_id"); + + b.Property("IsInvolved") + .HasColumnType("boolean") + .HasColumnName("is_involved"); + + b.Property("NexusModsModId") + .HasColumnType("integer") + .HasColumnName("nexusmods_mod_id"); + + b.Property("Version") + .IsRequired() + .HasColumnType("text") + .HasColumnName("version"); + + b.HasKey("TenantId", "CrashReportId", "ModuleId"); + + b.HasIndex("TenantId", "ModuleId"); + + b.HasIndex("TenantId", "NexusModsModId"); + + b.ToTable("crash_report_module_info", "crashreport"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.ExceptionTypeEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("ExceptionTypeId") + .HasColumnType("text") + .HasColumnName("exception_type_id"); + + b.HasKey("TenantId", "ExceptionTypeId"); + + b.ToTable("exception_type", "exception"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.IntegrationDiscordTokensEntity", b => + { + b.Property("NexusModsUserId") + .HasColumnType("integer") + .HasColumnName("integration_discord_tokens_id"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("text") + .HasColumnName("access_token"); + + b.Property("AccessTokenExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("access_token_expires_at"); + + b.Property("DiscordUserId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("discord_user_id"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.HasKey("NexusModsUserId"); + + b.HasIndex("DiscordUserId") + .IsUnique(); + + b.ToTable("integration_discord_tokens", "integration"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.IntegrationGOGToOwnedTenantEntity", b => + { + b.Property("GOGUserId") + .HasColumnType("text") + .HasColumnName("integration_gog_owned_tenant_id"); + + b.Property("OwnedTenant") + .HasColumnType("smallint") + .HasColumnName("owned_tenant"); + + b.HasKey("GOGUserId", "OwnedTenant"); + + b.HasIndex("OwnedTenant"); + + b.ToTable("integration_gog_owned_tenant", "integration"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.IntegrationGOGTokensEntity", b => + { + b.Property("NexusModsUserId") + .HasColumnType("integer") + .HasColumnName("integration_gog_tokens_id"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("text") + .HasColumnName("access_token"); + + b.Property("AccessTokenExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("access_token_expires_at"); + + b.Property("GOGUserId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("gog_user_id"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.HasKey("NexusModsUserId"); + + b.HasIndex("GOGUserId") + .IsUnique(); + + b.ToTable("integration_gog_tokens", "integration"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.IntegrationSteamToOwnedTenantEntity", b => + { + b.Property("SteamUserId") + .HasColumnType("text") + .HasColumnName("integration_steam_owned_tenant_id"); + + b.Property("OwnedTenant") + .HasColumnType("smallint") + .HasColumnName("owned_tenant"); + + b.HasKey("SteamUserId", "OwnedTenant"); + + b.HasIndex("OwnedTenant"); + + b.ToTable("integration_steam_owned_tenant", "integration"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.IntegrationSteamTokensEntity", b => + { + b.Property("NexusModsUserId") + .HasColumnType("integer") + .HasColumnName("integration_steam_tokens_id"); + + b.Property>("Data") + .IsRequired() + .HasColumnType("hstore") + .HasColumnName("data"); + + b.Property("SteamUserId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("steam_user_id"); + + b.HasKey("NexusModsUserId"); + + b.HasIndex("SteamUserId") + .IsUnique(); + + b.ToTable("integration_steam_tokens", "integration"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.ModuleEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("ModuleId") + .HasColumnType("text") + .HasColumnName("module_id"); + + b.HasKey("TenantId", "ModuleId"); + + b.ToTable("module", "module"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsArticleEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("NexusModsArticleId") + .HasColumnType("integer") + .HasColumnName("nexusmods_article_entity_id"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("create_date"); + + b.Property("NexusModsUserId") + .HasColumnType("integer") + .HasColumnName("author_id"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("TenantId", "NexusModsArticleId"); + + b.HasIndex("NexusModsUserId"); + + b.ToTable("nexusmods_article_entity", "nexusmods_article"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("NexusModsModId") + .HasColumnType("integer") + .HasColumnName("nexusmods_mod_id"); + + b.HasKey("TenantId", "NexusModsModId"); + + b.ToTable("nexusmods_mod", "nexusmods_mod"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModToFileUpdateEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("NexusModsModId") + .HasColumnType("integer") + .HasColumnName("nexusmods_mod_file_update_id"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_of_last_check"); + + b.HasKey("TenantId", "NexusModsModId"); + + b.ToTable("nexusmods_mod_file_update", "nexusmods_mod"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModToModuleEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("NexusModsModId") + .HasColumnType("integer") + .HasColumnName("nexusmods_mod_module_id"); + + b.Property("ModuleId") + .HasColumnType("text") + .HasColumnName("module_id"); + + b.Property("LinkType") + .HasColumnType("integer") + .HasColumnName("nexusmods_mod_module_link_type_id"); + + b.Property("LastUpdateDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_of_last_update"); + + b.HasKey("TenantId", "NexusModsModId", "ModuleId", "LinkType"); + + b.HasIndex("TenantId", "ModuleId"); + + b.ToTable("nexusmods_mod_module", "nexusmods_mod"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModToModuleInfoHistoryEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("NexusModsModId") + .HasColumnType("integer") + .HasColumnName("nexusmods_mod_name_id"); + + b.Property("ModuleId") + .HasColumnType("text") + .HasColumnName("module_id"); + + b.Property("ModuleVersion") + .HasColumnType("text") + .HasColumnName("module_version"); + + b.Property("ModuleInfo") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("module_info"); + + b.HasKey("TenantId", "NexusModsModId", "ModuleId", "ModuleVersion"); + + b.HasIndex("TenantId", "ModuleId"); + + b.ToTable("nexusmods_mod_module_info_history", "nexusmods_mod"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModToNameEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("NexusModsModId") + .HasColumnType("integer") + .HasColumnName("nexusmods_mod_name_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("TenantId", "NexusModsModId"); + + b.ToTable("nexusmods_mod_name", "nexusmods_mod"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserEntity", b => + { + b.Property("NexusModsUserId") + .HasColumnType("integer") + .HasColumnName("nexusmods_user_id"); + + b.HasKey("NexusModsUserId"); + + b.ToTable("nexusmods_user", "nexusmods_user"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToCrashReportEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("NexusModsUserId") + .HasColumnType("integer") + .HasColumnName("nexusmods_user_crash_report_id"); + + b.Property("CrashReportId") + .HasColumnType("uuid") + .HasColumnName("crash_report_id"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text") + .HasColumnName("comment"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("TenantId", "NexusModsUserId", "CrashReportId"); + + b.HasIndex("NexusModsUserId"); + + b.HasIndex("TenantId", "CrashReportId"); + + b.ToTable("nexusmods_user_crash_report", "nexusmods_user"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationDiscordEntity", b => + { + b.Property("NexusModsUserId") + .HasColumnType("integer") + .HasColumnName("nexusmods_user_to_discord_id"); + + b.Property("DiscordUserId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("discord_user_id"); + + b.HasKey("NexusModsUserId"); + + b.ToTable("nexusmods_user_to_integration_discord", "nexusmods_user"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationGOGEntity", b => + { + b.Property("NexusModsUserId") + .HasColumnType("integer") + .HasColumnName("nexusmods_user_to_gog_id"); + + b.Property("GOGUserId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("gog_user_id"); + + b.HasKey("NexusModsUserId"); + + b.ToTable("nexusmods_user_to_integration_gog", "nexusmods_user"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationSteamEntity", b => + { + b.Property("NexusModsUserId") + .HasColumnType("integer") + .HasColumnName("nexusmods_user_to_steam_id"); + + b.Property("SteamUserId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("steam_user_id"); + + b.HasKey("NexusModsUserId"); + + b.ToTable("nexusmods_user_to_integration_steam", "nexusmods_user"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToModuleEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("NexusModsUserId") + .HasColumnType("integer") + .HasColumnName("nexusmods_user_module_id"); + + b.Property("ModuleId") + .HasColumnType("text") + .HasColumnName("module_id"); + + b.Property("LinkType") + .HasColumnType("integer") + .HasColumnName("nexusmods_user_module_link_type_id"); + + b.HasKey("TenantId", "NexusModsUserId", "ModuleId", "LinkType"); + + b.HasIndex("NexusModsUserId"); + + b.HasIndex("TenantId", "ModuleId"); + + b.ToTable("nexusmods_user_module", "nexusmods_user"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToNameEntity", b => + { + b.Property("NexusModsUserId") + .HasColumnType("integer") + .HasColumnName("nexusmods_user_name_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("NexusModsUserId"); + + b.ToTable("nexusmods_user_name", "nexusmods_user"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToNexusModsModEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("NexusModsUserId") + .HasColumnType("integer") + .HasColumnName("nexusmods_user_nexusmods_mod_id"); + + b.Property("NexusModsModId") + .HasColumnType("integer") + .HasColumnName("nexusmods_mod_id"); + + b.Property("LinkType") + .HasColumnType("integer") + .HasColumnName("nexusmods_user_nexusmods_mod_link_type_id"); + + b.HasKey("TenantId", "NexusModsUserId", "NexusModsModId", "LinkType"); + + b.HasIndex("NexusModsUserId"); + + b.HasIndex("TenantId", "NexusModsModId"); + + b.ToTable("nexusmods_user_nexusmods_mod", "nexusmods_user"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToRoleEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("NexusModsUserId") + .HasColumnType("integer") + .HasColumnName("nexusmods_user_role_id"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role"); + + b.HasKey("TenantId", "NexusModsUserId"); + + b.HasIndex("NexusModsUserId"); + + b.ToTable("nexusmods_user_role", "nexusmods_user"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.QuartzExecutionLogEntity", b => + { + b.Property("LogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("quartz_execution_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LogId")); + + b.Property("DateAddedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_added_utc"); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.Property("ExecutionLogDetail") + .HasColumnType("jsonb") + .HasColumnName("execution_log_detail"); + + b.Property("FireTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("fire_time_utc"); + + b.Property("IsException") + .HasColumnType("boolean") + .HasColumnName("is_exception"); + + b.Property("IsSuccess") + .HasColumnType("boolean") + .HasColumnName("is_success"); + + b.Property("IsVetoed") + .HasColumnType("boolean") + .HasColumnName("is_vetoed"); + + b.Property("JobGroup") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("job_name"); + + b.Property("JobRunTime") + .HasColumnType("interval") + .HasColumnName("job_run_time"); + + b.Property("LogType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("log_type"); + + b.Property("MachineName") + .HasColumnType("text") + .HasColumnName("machie_name"); + + b.Property("Result") + .HasColumnType("text") + .HasColumnName("result"); + + b.Property("RetryCount") + .HasColumnType("integer") + .HasColumnName("retry_count"); + + b.Property("ReturnCode") + .HasColumnType("text") + .HasColumnName("return_code"); + + b.Property("RunInstanceId") + .HasColumnType("text") + .HasColumnName("run_instance_id"); + + b.Property("ScheduleFireTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("_schedule_fire_time_utc"); + + b.Property("TriggerGroup") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("trigger_name"); + + b.HasKey("LogId"); + + b.HasIndex("RunInstanceId"); + + b.HasIndex("DateAddedUtc", "LogType"); + + b.HasIndex("TriggerName", "TriggerGroup", "JobName", "JobGroup", "DateAddedUtc"); + + b.ToTable("quartz_execution_log", "quartz"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.StatisticsCrashScoreInvolvedEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("StatisticsCrashScoreInvolvedId") + .HasColumnType("uuid") + .HasColumnName("crash_score_involved_id"); + + b.Property("GameVersion") + .IsRequired() + .HasColumnType("text") + .HasColumnName("game_version"); + + b.Property("InvolvedCount") + .HasColumnType("integer") + .HasColumnName("involved_count"); + + b.Property("ModuleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("module_id"); + + b.Property("ModuleVersion") + .IsRequired() + .HasColumnType("text") + .HasColumnName("module_version"); + + b.Property("NotInvolvedCount") + .HasColumnType("integer") + .HasColumnName("not_involved_count"); + + b.Property("RawValue") + .HasColumnType("integer") + .HasColumnName("value"); + + b.Property("Score") + .HasColumnType("double precision") + .HasColumnName("crash_score"); + + b.Property("TotalCount") + .HasColumnType("integer") + .HasColumnName("total_count"); + + b.HasKey("TenantId", "StatisticsCrashScoreInvolvedId"); + + b.HasIndex("TenantId", "ModuleId"); + + b.ToTable("crash_score_involved", "statistics"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.StatisticsTopExceptionsTypeEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("ExceptionTypeId") + .HasColumnType("text") + .HasColumnName("top_exceptions_type_id"); + + b.Property("ExceptionCount") + .HasColumnType("integer") + .HasColumnName("count"); + + b.HasKey("TenantId", "ExceptionTypeId"); + + b.ToTable("top_exceptions_type", "statistics"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant_id"); + + b.HasKey("TenantId"); + + b.ToTable("tenant", "tenant"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.AutocompleteEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.CrashReportEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.ExceptionTypeEntity", "ExceptionType") + .WithMany("ToCrashReports") + .HasForeignKey("TenantId", "ExceptionTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExceptionType"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.CrashReportIgnoredFileEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.CrashReportToFileIdEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.CrashReportEntity", "ToCrashReport") + .WithOne("FileId") + .HasForeignKey("BUTR.Site.NexusMods.Server.Models.Database.CrashReportToFileIdEntity", "TenantId", "CrashReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ToCrashReport"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.CrashReportToMetadataEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.CrashReportEntity", "ToCrashReport") + .WithOne("Metadata") + .HasForeignKey("BUTR.Site.NexusMods.Server.Models.Database.CrashReportToMetadataEntity", "TenantId", "CrashReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ToCrashReport"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.CrashReportToModuleMetadataEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.CrashReportEntity", "ToCrashReport") + .WithMany("ModuleInfos") + .HasForeignKey("TenantId", "CrashReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.ModuleEntity", "Module") + .WithMany() + .HasForeignKey("TenantId", "ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModEntity", "NexusModsMod") + .WithMany() + .HasForeignKey("TenantId", "NexusModsModId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Module"); + + b.Navigation("NexusModsMod"); + + b.Navigation("ToCrashReport"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.ExceptionTypeEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.IntegrationDiscordTokensEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationDiscordEntity", "UserToDiscord") + .WithOne("ToTokens") + .HasForeignKey("BUTR.Site.NexusMods.Server.Models.Database.IntegrationDiscordTokensEntity", "DiscordUserId") + .HasPrincipalKey("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationDiscordEntity", "DiscordUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserEntity", "NexusModsUser") + .WithOne() + .HasForeignKey("BUTR.Site.NexusMods.Server.Models.Database.IntegrationDiscordTokensEntity", "NexusModsUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NexusModsUser"); + + b.Navigation("UserToDiscord"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.IntegrationGOGToOwnedTenantEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationGOGEntity", null) + .WithMany("ToOwnedTenants") + .HasForeignKey("GOGUserId") + .HasPrincipalKey("GOGUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("OwnedTenant") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.IntegrationGOGTokensEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationGOGEntity", "UserToGOG") + .WithOne("ToTokens") + .HasForeignKey("BUTR.Site.NexusMods.Server.Models.Database.IntegrationGOGTokensEntity", "GOGUserId") + .HasPrincipalKey("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationGOGEntity", "GOGUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserEntity", "NexusModsUser") + .WithOne() + .HasForeignKey("BUTR.Site.NexusMods.Server.Models.Database.IntegrationGOGTokensEntity", "NexusModsUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NexusModsUser"); + + b.Navigation("UserToGOG"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.IntegrationSteamToOwnedTenantEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("OwnedTenant") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationSteamEntity", null) + .WithMany("ToOwnedTenants") + .HasForeignKey("SteamUserId") + .HasPrincipalKey("SteamUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.IntegrationSteamTokensEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserEntity", "NexusModsUser") + .WithOne() + .HasForeignKey("BUTR.Site.NexusMods.Server.Models.Database.IntegrationSteamTokensEntity", "NexusModsUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationSteamEntity", "UserToSteam") + .WithOne("ToTokens") + .HasForeignKey("BUTR.Site.NexusMods.Server.Models.Database.IntegrationSteamTokensEntity", "SteamUserId") + .HasPrincipalKey("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationSteamEntity", "SteamUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NexusModsUser"); + + b.Navigation("UserToSteam"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.ModuleEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsArticleEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserEntity", "NexusModsUser") + .WithMany("ToArticles") + .HasForeignKey("NexusModsUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NexusModsUser"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModToFileUpdateEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModEntity", "NexusModsMod") + .WithOne("FileUpdate") + .HasForeignKey("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModToFileUpdateEntity", "TenantId", "NexusModsModId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NexusModsMod"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModToModuleEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.ModuleEntity", "Module") + .WithMany("ToNexusModsMods") + .HasForeignKey("TenantId", "ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModEntity", "NexusModsMod") + .WithMany("ModuleIds") + .HasForeignKey("TenantId", "NexusModsModId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + + b.Navigation("NexusModsMod"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModToModuleInfoHistoryEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.ModuleEntity", "Module") + .WithMany() + .HasForeignKey("TenantId", "ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModEntity", "NexusModsMod") + .WithMany() + .HasForeignKey("TenantId", "NexusModsModId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + + b.Navigation("NexusModsMod"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModToNameEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModEntity", "NexusModsMod") + .WithOne("Name") + .HasForeignKey("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModToNameEntity", "TenantId", "NexusModsModId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NexusModsMod"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToCrashReportEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserEntity", "NexusModsUser") + .WithMany("ToCrashReports") + .HasForeignKey("NexusModsUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.CrashReportEntity", "ToCrashReport") + .WithMany("ToUsers") + .HasForeignKey("TenantId", "CrashReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NexusModsUser"); + + b.Navigation("ToCrashReport"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationDiscordEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserEntity", "NexusModsUser") + .WithOne("ToDiscord") + .HasForeignKey("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationDiscordEntity", "NexusModsUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NexusModsUser"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationGOGEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserEntity", "NexusModsUser") + .WithOne("ToGOG") + .HasForeignKey("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationGOGEntity", "NexusModsUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NexusModsUser"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationSteamEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserEntity", "NexusModsUser") + .WithOne("ToSteam") + .HasForeignKey("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationSteamEntity", "NexusModsUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NexusModsUser"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToModuleEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserEntity", "NexusModsUser") + .WithMany("ToModules") + .HasForeignKey("NexusModsUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.ModuleEntity", "Module") + .WithMany("ToNexusModsUsers") + .HasForeignKey("TenantId", "ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + + b.Navigation("NexusModsUser"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToNameEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserEntity", "NexusModsUser") + .WithOne("Name") + .HasForeignKey("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToNameEntity", "NexusModsUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NexusModsUser"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToNexusModsModEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserEntity", "NexusModsUser") + .WithMany("ToNexusModsMods") + .HasForeignKey("NexusModsUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModEntity", "NexusModsMod") + .WithMany("ToNexusModsUsers") + .HasForeignKey("TenantId", "NexusModsModId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NexusModsMod"); + + b.Navigation("NexusModsUser"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToRoleEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserEntity", "NexusModsUser") + .WithMany("ToRoles") + .HasForeignKey("NexusModsUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NexusModsUser"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.StatisticsCrashScoreInvolvedEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.ModuleEntity", "Module") + .WithMany("ToCrashScore") + .HasForeignKey("TenantId", "ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.StatisticsTopExceptionsTypeEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.ExceptionTypeEntity", "ExceptionType") + .WithMany("ToTopExceptionsTypes") + .HasForeignKey("TenantId", "ExceptionTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExceptionType"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.CrashReportEntity", b => + { + b.Navigation("FileId"); + + b.Navigation("Metadata"); + + b.Navigation("ModuleInfos"); + + b.Navigation("ToUsers"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.ExceptionTypeEntity", b => + { + b.Navigation("ToCrashReports"); + + b.Navigation("ToTopExceptionsTypes"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.ModuleEntity", b => + { + b.Navigation("ToCrashScore"); + + b.Navigation("ToNexusModsMods"); + + b.Navigation("ToNexusModsUsers"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModEntity", b => + { + b.Navigation("FileUpdate"); + + b.Navigation("ModuleIds"); + + b.Navigation("Name"); + + b.Navigation("ToNexusModsUsers"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserEntity", b => + { + b.Navigation("Name"); + + b.Navigation("ToArticles"); + + b.Navigation("ToCrashReports"); + + b.Navigation("ToDiscord"); + + b.Navigation("ToGOG"); + + b.Navigation("ToModules"); + + b.Navigation("ToNexusModsMods"); + + b.Navigation("ToRoles"); + + b.Navigation("ToSteam"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationDiscordEntity", b => + { + b.Navigation("ToTokens"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationGOGEntity", b => + { + b.Navigation("ToOwnedTenants"); + + b.Navigation("ToTokens"); + }); + + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsUserToIntegrationSteamEntity", b => + { + b.Navigation("ToOwnedTenants"); + + b.Navigation("ToTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/BUTR.Site.NexusMods.Server/Migrations/20231006075646_NexusModsModToModuleInfoHistory.cs b/src/BUTR.Site.NexusMods.Server/Migrations/20231006075646_NexusModsModToModuleInfoHistory.cs new file mode 100644 index 00000000..508fba10 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Migrations/20231006075646_NexusModsModToModuleInfoHistory.cs @@ -0,0 +1,66 @@ +using BUTR.Site.NexusMods.Server.Models.Database; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BUTR.Site.NexusMods.Server.Migrations +{ + /// + public partial class NexusModsModToModuleInfoHistory : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "nexusmods_mod_module_info_history", + schema: "nexusmods_mod", + columns: table => new + { + tenant = table.Column(type: "smallint", nullable: false), + module_version = table.Column(type: "text", nullable: false), + nexusmods_mod_name_id = table.Column(type: "integer", nullable: false), + module_id = table.Column(type: "text", nullable: false), + module_info = table.Column(type: "jsonb", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_nexusmods_mod_module_info_history", x => new { x.tenant, x.nexusmods_mod_name_id, x.module_id, x.module_version }); + table.ForeignKey( + name: "FK_nexusmods_mod_module_info_history_module_tenant_module_id", + columns: x => new { x.tenant, x.module_id }, + principalSchema: "module", + principalTable: "module", + principalColumns: new[] { "tenant", "module_id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_nexusmods_mod_module_info_history_nexusmods_mod_tenant_nexu~", + columns: x => new { x.tenant, x.nexusmods_mod_name_id }, + principalSchema: "nexusmods_mod", + principalTable: "nexusmods_mod", + principalColumns: new[] { "tenant", "nexusmods_mod_id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_nexusmods_mod_module_info_history_tenant_tenant", + column: x => x.tenant, + principalSchema: "tenant", + principalTable: "tenant", + principalColumn: "tenant_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_nexusmods_mod_module_info_history_tenant_module_id", + schema: "nexusmods_mod", + table: "nexusmods_mod_module_info_history", + columns: new[] { "tenant", "module_id" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "nexusmods_mod_module_info_history", + schema: "nexusmods_mod"); + } + } +} diff --git a/src/BUTR.Site.NexusMods.Server/Migrations/BaseAppDbContextModelSnapshot.cs b/src/BUTR.Site.NexusMods.Server/Migrations/BaseAppDbContextModelSnapshot.cs index fb6314c3..167b5867 100644 --- a/src/BUTR.Site.NexusMods.Server/Migrations/BaseAppDbContextModelSnapshot.cs +++ b/src/BUTR.Site.NexusMods.Server/Migrations/BaseAppDbContextModelSnapshot.cs @@ -19,7 +19,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0-preview.7.23375.4") + .HasAnnotation("ProductVersion", "8.0.0-rc.1.23419.6") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); @@ -457,6 +457,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("nexusmods_mod_module", "nexusmods_mod"); }); + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModToModuleInfoHistoryEntity", b => + { + b.Property("TenantId") + .HasColumnType("smallint") + .HasColumnName("tenant"); + + b.Property("NexusModsModId") + .HasColumnType("integer") + .HasColumnName("nexusmods_mod_name_id"); + + b.Property("ModuleId") + .HasColumnType("text") + .HasColumnName("module_id"); + + b.Property("ModuleVersion") + .HasColumnType("text") + .HasColumnName("module_version"); + + b.Property("ModuleInfo") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("module_info"); + + b.HasKey("TenantId", "NexusModsModId", "ModuleId", "ModuleVersion"); + + b.HasIndex("TenantId", "ModuleId"); + + b.ToTable("nexusmods_mod_module_info_history", "nexusmods_mod"); + }); + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModToNameEntity", b => { b.Property("TenantId") @@ -1123,6 +1153,31 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("NexusModsMod"); }); + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModToModuleInfoHistoryEntity", b => + { + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.ModuleEntity", "Module") + .WithMany() + .HasForeignKey("TenantId", "ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModEntity", "NexusModsMod") + .WithMany() + .HasForeignKey("TenantId", "NexusModsModId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + + b.Navigation("NexusModsMod"); + }); + modelBuilder.Entity("BUTR.Site.NexusMods.Server.Models.Database.NexusModsModToNameEntity", b => { b.HasOne("BUTR.Site.NexusMods.Server.Models.Database.TenantEntity", null) diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/Json/ApplicationVersionRangeModel.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/Json/ApplicationVersionRangeModel.cs new file mode 100644 index 00000000..aeceaa23 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/Json/ApplicationVersionRangeModel.cs @@ -0,0 +1,7 @@ +namespace BUTR.Site.NexusMods.Server.Models.Database; + +public sealed record ApplicationVersionRangeModel +{ + public required string? Min { get; init; } + public required string? Max { get; init; } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/Json/DependentModuleModel.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/Json/DependentModuleModel.cs new file mode 100644 index 00000000..b26224da --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/Json/DependentModuleModel.cs @@ -0,0 +1,10 @@ +namespace BUTR.Site.NexusMods.Server.Models.Database; + +public sealed record DependentModuleModel +{ + public required string Id { get; init; } + public required string Type { get; init; } + public required bool IsOptional { get; init; } + public required string? Version { get; init; } + public required ApplicationVersionRangeModel? VersionRange { get; init; } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/Json/ModuleInfoModel.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/Json/ModuleInfoModel.cs new file mode 100644 index 00000000..a70806f0 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/Json/ModuleInfoModel.cs @@ -0,0 +1,57 @@ +using Bannerlord.ModuleManager; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Models.Database; + +public sealed record ModuleInfoModel +{ + public required string Id { get; init; } + public required string Name { get; init; } + public required bool IsOfficial { get; init; } + public required string Version { get; init; } + public required bool IsSingleplayerModule { get; init; } + public required bool IsMultiplayerModule { get; init; } + public required bool IsServerModule { get; init; } + public required string Url { get; init; } + public required string UpdateInfo { get; init; } + public required DependentModuleModel[] DependentModuleMetadatas { get; init; } + public required SubModuleInfoModel[] SubModules { get; init; } + + public static ModuleInfoModel Create(ModuleInfoExtended moduleInfo) => new() + { + Id = moduleInfo.Id, + Name = moduleInfo.Name, + IsOfficial = moduleInfo.IsOfficial, + Version = moduleInfo.Version.ToString(), + IsSingleplayerModule = moduleInfo.IsSingleplayerModule, + IsMultiplayerModule = moduleInfo.IsMultiplayerModule, + IsServerModule = moduleInfo.IsServerModule, + Url = moduleInfo.Url, + UpdateInfo = moduleInfo.UpdateInfo, + DependentModuleMetadatas = moduleInfo.DependenciesAllDistinct().Select(y => new DependentModuleModel + { + Id = y.Id, + Type = y.IsIncompatible ? "Incompatible" : y.LoadType == LoadType.LoadAfterThis ? "LoadAfterThis" : "LoadBeforeThis", + IsOptional = y.IsOptional, + Version = y.Version != ApplicationVersion.Empty ? y.Version.ToString() : null, + VersionRange = y.VersionRange != ApplicationVersionRange.Empty ? new() + { + Min = y.VersionRange.Min != ApplicationVersion.Empty ? y.VersionRange.Min.ToString() : null, + Max = y.VersionRange.Max != ApplicationVersion.Empty ? y.VersionRange.Max.ToString() : null + } : null, + }).ToArray(), + SubModules = moduleInfo.SubModules.Select(y => new SubModuleInfoModel + { + Name = y.Name, + DLLName = y.DLLName, + Assemblies = y.Assemblies.ToArray(), + SubModuleClassType = y.SubModuleClassType, + Tags = y.Tags.Select(z => new SubModuleInfoTagModel + { + Key = z.Key, + Value = z.Value.ToArray(), + }).ToArray(), + }).ToArray(), + }; +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/Json/SubModuleInfoModel.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/Json/SubModuleInfoModel.cs new file mode 100644 index 00000000..3a5df086 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/Json/SubModuleInfoModel.cs @@ -0,0 +1,10 @@ +namespace BUTR.Site.NexusMods.Server.Models.Database; + +public sealed record SubModuleInfoModel +{ + public required string Name { get; init; } + public required string DLLName { get; init; } + public required string[] Assemblies { get; init; } + public required string SubModuleClassType { get; init; } + public required SubModuleInfoTagModel[] Tags { get; init; } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/Json/SubModuleInfoTagModel.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/Json/SubModuleInfoTagModel.cs new file mode 100644 index 00000000..f6ea218f --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/Json/SubModuleInfoTagModel.cs @@ -0,0 +1,7 @@ +namespace BUTR.Site.NexusMods.Server.Models.Database; + +public sealed record SubModuleInfoTagModel +{ + public required string Key { get; init; } + public required string[] Value { get; init; } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleInfoHistoryEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleInfoHistoryEntity.cs new file mode 100644 index 00000000..77b57d0d --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleInfoHistoryEntity.cs @@ -0,0 +1,16 @@ +using System; + +namespace BUTR.Site.NexusMods.Server.Models.Database; + +public sealed record NexusModsModToModuleInfoHistoryEntity : IEntityWithTenant +{ + public required TenantId TenantId { get; init; } + public required NexusModsModEntity NexusModsMod { get; init; } + public required ModuleEntity Module { get; init; } + public required ModuleVersion ModuleVersion { get; init; } + + public required ModuleInfoModel ModuleInfo { get; init; } + + + public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsMod.NexusModsModId, Module.ModuleId, ModuleVersion); +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Services/NexusModsInfo.cs b/src/BUTR.Site.NexusMods.Server/Services/NexusModsModFileParser.cs similarity index 64% rename from src/BUTR.Site.NexusMods.Server/Services/NexusModsInfo.cs rename to src/BUTR.Site.NexusMods.Server/Services/NexusModsModFileParser.cs index dc73f4dd..b0eff9b8 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/NexusModsInfo.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/NexusModsModFileParser.cs @@ -1,4 +1,6 @@ -using BUTR.Site.NexusMods.Server.Extensions; +using Bannerlord.ModuleManager; + +using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.NexusModsAPI; using BUTR.Site.NexusMods.Server.Utils; @@ -17,36 +19,23 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using System.Xml.Serialization; +using System.Xml; namespace BUTR.Site.NexusMods.Server.Services; -public class NexusModsInfo +public class NexusModsModFileParser { - [XmlRoot("Module")] - public record SubModuleXml - { - public record IdValue - { - [XmlAttribute("value")] - public required string Value { get; init; } - } - - [XmlElement("Id")] - public required IdValue Id { get; init; } - } - private readonly HttpClient _httpClient; private readonly NexusModsAPIClient _apiClient; - public NexusModsInfo(HttpClient httpClient, NexusModsAPIClient apiClient) + public NexusModsModFileParser(HttpClient httpClient, NexusModsAPIClient apiClient) { _httpClient = httpClient; _apiClient = apiClient; } - public async IAsyncEnumerable GetModIdsAsync(NexusModsGameDomain gameDomain, NexusModsModId modId, NexusModsApiKey apiKey, [EnumeratorCancellation] CancellationToken ct) + public async IAsyncEnumerable GetModuleInfosAsync(NexusModsGameDomain gameDomain, NexusModsModId modId, NexusModsApiKey apiKey, [EnumeratorCancellation] CancellationToken ct) { const int DefaultBufferSize = 1024 * 16; const int LargeBufferSize = 1024 * 1024 * 5; @@ -69,8 +58,8 @@ public async IAsyncEnumerable GetModIdsAsync(NexusModsGameDomain gameD httpStream.SetBufferSize(LargeBufferSize); using var reader = ReaderExtensions.OpenOrDefault(httpStream, new ReaderOptions { LeaveStreamOpen = true }); if (reader is null) throw new InvalidOperationException($"Failed to get Reader for file '{fileInfo.FileName}'"); - await foreach (var id in GetModIdsFromReaderAsync(reader).WithCancellation(ct)) - yield return ModuleId.From(id); + await foreach (var moduleInfo in GetModuleInfosFromReaderAsync(reader).WithCancellation(ct)) + yield return moduleInfo; continue; } @@ -80,28 +69,12 @@ public async IAsyncEnumerable GetModIdsAsync(NexusModsGameDomain gameD if (archive.Type == ArchiveType.Rar) httpStream.SetBufferSize(LargeBufferSize); - await foreach (var id in GetModIdsFromArchiveAsync(archive).WithCancellation(ct)) - yield return ModuleId.From(id); - } - } - - - private static bool ContainsSubModuleFile(IReadOnlyList? entries) - { - if (entries is null) - return false; - - foreach (var entry in entries) - { - if (entry.Name.Equals("SubModule.xml", StringComparison.OrdinalIgnoreCase)) - return true; - if (ContainsSubModuleFile(entry.Children)) - return true; + await foreach (var moduleInfo in GetModuleInfosFromArchiveAsync(archive).WithCancellation(ct)) + yield return moduleInfo; } - return false; } - private static async IAsyncEnumerable GetModIdsFromReaderAsync(IReader reader) + private static async IAsyncEnumerable GetModuleInfosFromReaderAsync(IReader reader) { while (reader.MoveToNextEntry()) { @@ -111,14 +84,14 @@ private static async IAsyncEnumerable GetModIdsFromReaderAsync(IReader r await using var stream = reader.OpenEntryStream(); - if (GetSubModuleId(stream) is not { } id) continue; + if (GetModuleInfo(stream) is not { } moduleInfo) continue; - yield return id; + yield return moduleInfo; break; } } - private static async IAsyncEnumerable GetModIdsFromArchiveAsync(IArchive archive) + private static async IAsyncEnumerable GetModuleInfosFromArchiveAsync(IArchive archive) { foreach (var entry in archive.Entries) { @@ -127,15 +100,30 @@ private static async IAsyncEnumerable GetModIdsFromArchiveAsync(IArchive if (!entry.Key.Contains("SubModule.xml", StringComparison.OrdinalIgnoreCase)) continue; await using var stream = entry.OpenEntryStream(); - if (GetSubModuleId(stream) is not { } id) continue; + if (GetModuleInfo(stream) is not { } moduleInfo) continue; - yield return id; + yield return moduleInfo; break; } } private async Task HasSubModuleXmlAsync(NexusModsModFilesResponse.File fileInfo) { + static bool ContainsSubModuleFile(IReadOnlyList? entries) + { + if (entries is null) + return false; + + foreach (var entry in entries) + { + if (entry.Name.Equals("SubModule.xml", StringComparison.OrdinalIgnoreCase)) + return true; + if (ContainsSubModuleFile(entry.Children)) + return true; + } + return false; + } + try { var content = await _httpClient.GetFromJsonAsync(fileInfo.ContentPreviewUrl); @@ -149,18 +137,17 @@ private async Task HasSubModuleXmlAsync(NexusModsModFilesResponse.File fil return true; } - private static string? GetSubModuleId(Stream stream) + private static ModuleInfoExtended? GetModuleInfo(Stream stream) { try { - using var streamReader = new StreamReader(stream); - var serializer = new XmlSerializer(typeof(SubModuleXml)); - if (serializer.Deserialize(streamReader) is not SubModuleXml result) return null; - return result.Id.Value; + var document = new XmlDocument(); + document.Load(stream); + return ModuleInfoExtended.FromXml(document); } catch (Exception) { - return "ERROR"; + return null; } } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Startup.cs b/src/BUTR.Site.NexusMods.Server/Startup.cs index 2d54f03b..9a55a8d3 100644 --- a/src/BUTR.Site.NexusMods.Server/Startup.cs +++ b/src/BUTR.Site.NexusMods.Server/Startup.cs @@ -224,7 +224,7 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); - services.AddTransient(); + services.AddTransient(); services.AddSingleton(); services.AddAuthentication(ButrNexusModsAuthSchemeConstants.AuthScheme).AddNexusMods(options =>