diff --git a/.gitignore b/.gitignore index 33f113a8..9df21979 100644 --- a/.gitignore +++ b/.gitignore @@ -350,7 +350,7 @@ MigrationBackup/ .ionide/ *.sqlite3 !/build/* -/src/swagger.json +/src/BUTR.Site.NexusMods.Server/BUTR.Site.NexusMods.Server.json /src/BUTR.Site.NexusMods.Server/.config/dotnet-tools.json /**/*.g.cs /src/BUTR.Site.NexusMods.ServerClient/.config/dotnet-tools.json diff --git a/src/BUTR.Site.NexusMods.Client/BUTR.Site.NexusMods.Client.csproj b/src/BUTR.Site.NexusMods.Client/BUTR.Site.NexusMods.Client.csproj index c20c974d..e9c0e8b8 100644 --- a/src/BUTR.Site.NexusMods.Client/BUTR.Site.NexusMods.Client.csproj +++ b/src/BUTR.Site.NexusMods.Client/BUTR.Site.NexusMods.Client.csproj @@ -17,25 +17,25 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + diff --git a/src/BUTR.Site.NexusMods.Client/Components/Grid/DataGridPaging.razor b/src/BUTR.Site.NexusMods.Client/Components/Grid/DataGridPaging.razor index 19004352..244aea78 100644 --- a/src/BUTR.Site.NexusMods.Client/Components/Grid/DataGridPaging.razor +++ b/src/BUTR.Site.NexusMods.Client/Components/Grid/DataGridPaging.razor @@ -8,12 +8,11 @@ SelectedRowChanged="@(model => { DataGridUtils.SelectDeselect(model, ref Value, ref _dataGridRef); })" ReadData="@OnReadData" TotalItems="@Metadata.TotalCount" - PageSize="@Metadata.PageSize" + PageSizes="@PageSizes" SelectionMode="@DataGridSelectionMode.Single" ShowPager ShowPageSizes PagerOptions="@(new DataGridPagerOptions { PaginationPosition = PagerElementPosition.Center, ButtonRowPosition = PagerElementPosition.Start, TotalItemsPosition = PagerElementPosition.End })" - PageSizes="@PageSizes" Filterable="@Filterable" Sortable="@Sortable" Responsive @@ -133,6 +132,7 @@ .Select(x => new Sorting(x.SortField, x.SortDirection.ToSortingType())) .ToArray(); var filterings = GetFilters is not null ? GetFilters(e.Columns).ToArray() : Array.Empty(); + await LoadItems(e.Page, e.PageSize, filterings, sortings, CancellationToken.None); } } diff --git a/src/BUTR.Site.NexusMods.Client/Components/Grid/DataGridStreamingPaging.razor b/src/BUTR.Site.NexusMods.Client/Components/Grid/DataGridStreamingPaging.razor index dfddb6ed..329c1fc5 100644 --- a/src/BUTR.Site.NexusMods.Client/Components/Grid/DataGridStreamingPaging.razor +++ b/src/BUTR.Site.NexusMods.Client/Components/Grid/DataGridStreamingPaging.razor @@ -8,7 +8,6 @@ SelectedRowChanged="@(model => { DataGridUtils.SelectDeselect(model, ref Value, ref _dataGridRef); })" ReadData="@OnReadData" CurrentPage="@Metadata.CurrentPage" - PageSize="@Metadata.PageSize" PageSizes="@PageSizes" TotalItems="@Metadata.TotalCount" SelectionMode="@DataGridSelectionMode.Single" diff --git a/src/BUTR.Site.NexusMods.Client/Components/Grid/DataGridVirtual.razor b/src/BUTR.Site.NexusMods.Client/Components/Grid/DataGridVirtual.razor new file mode 100644 index 00000000..c2c0a316 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Client/Components/Grid/DataGridVirtual.razor @@ -0,0 +1,136 @@ +@typeparam TItem where TItem : class + +@inject ILocalStorageService _localStorage + + + + +@code { + + public sealed record ItemsResponse(PagingMetadata Metadata, ICollection Items, PagingAdditionalMetadata AdditionalMetadata); + + [Parameter] + public bool Resizable { get; set; } = false; + [Parameter] + public TableResizeMode ResizeMode { get; set; } + + [Parameter] + public bool Editable { get; set; } = false; + [Parameter] + public DataGridEditMode EditMode { get; set; } + + [Parameter] + public bool Filterable { get; set; } = false; + + [Parameter] + public bool Sortable { get; set; } = false; + + [Parameter] + public bool FixedHeader { get; set; } = false; + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } + + [Parameter] + public RenderFragment? DataGridColumns { get; set; } + + [Parameter] + public RenderFragment>? ButtonRowTemplate { get; set; } + + [Parameter] + public RenderFragment? DetailRowTemplate { get; set; } + + [Parameter] + public Func, bool>? DetailRowTrigger { get; set; } + + [Parameter] + public Func, IEnumerable>? GetFilters { get; set; } + + [Parameter] + public Func, ICollection, CancellationToken, Task>? GetItems { get; set; } + + [Parameter] + public int DefaultPageSize { get; set; } = UserSettings.DefaultPageSize; + + [Parameter] + public IEnumerable PageSizes { get; set; } = UserSettings.AvailablePageSizes; + + [Parameter] + public Func? GetPageSize { get; set; } = (settings => settings.PageSize); + + [Parameter] + public PagingMetadata Metadata { get; set; } + + [Parameter] + public PagingAdditionalMetadata AdditionalMetadata { get; set; } + + public TItem? Value; + public ICollection Values = default!; + + private int _progressValue = default!; + + private DataGrid _dataGridRef = default!; + private VirtualizeOptions virtualizeOptions; + + public DataGridVirtual() + { + Metadata = new PagingMetadata(1, 0, DefaultPageSize, 0); + AdditionalMetadata = PagingAdditionalMetadata.Empty; + } + + protected override async Task OnInitializedAsync() + { + virtualizeOptions = new() { DataGridHeight = "500px" }; + Metadata = new(1, 0, 0, 0); + } + + public Task Reload() => _dataGridRef.Reload(); + + private async Task OnReadData(DataGridReadDataEventArgs e) + { + if (!e.CancellationToken.IsCancellationRequested) + { + var sortings = e.Columns + .Where(x => x.SortIndex != -1) + .OrderBy(x => x.SortIndex) + .Select(x => new Sorting(x.SortField, x.SortDirection.ToSortingType())) + .ToArray(); + var filterings = GetFilters is not null ? GetFilters(e.Columns).ToArray() : Array.Empty(); + + var page = e.VirtualizeOffset / e.VirtualizeCount + 1; + var pageSize = e.VirtualizeCount; + await LoadItems(page, pageSize, filterings, sortings, CancellationToken.None); + } + } + + private async Task LoadItems(int page, int pageSize, ICollection filterings, ICollection sortings, CancellationToken ct = default) + { + var response = (GetItems is not null ? await GetItems(page, pageSize, filterings, sortings, ct) : null) ?? new(PagingMetadata.Empty, Array.Empty(), PagingAdditionalMetadata.Empty); + Metadata = response.Metadata; + Values = response.Items; + AdditionalMetadata = response.AdditionalMetadata; + } + +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Client/Models/DemoUser.cs b/src/BUTR.Site.NexusMods.Client/Models/DemoUser.cs index 41c3711b..eecc98ca 100644 --- a/src/BUTR.Site.NexusMods.Client/Models/DemoUser.cs +++ b/src/BUTR.Site.NexusMods.Client/Models/DemoUser.cs @@ -22,23 +22,23 @@ public static class DemoUser isSupporter: true, isPremium: true, role: ApplicationRoles.User, - steamUserId: null, - gogUserId: null, - discordUserId: null, - gitHubUserId: null, + steamUserId: null!, + gogUserId: null!, + discordUserId: null!, + gitHubUserId: null!, hasTenantGame: true, availableTenants: new List { new(tenantId: 1, name: "Bannerlord") }); - private static readonly List _mods = new() + private static readonly List _mods = new() { - new(nexusModsModId: 1, name: "Demo Mod 1", allowedNexusModsUserIds: Array.Empty(), manuallyLinkedNexusModsUserIds: Array.Empty(), knownModuleIds: Array.Empty(), manuallyLinkedModuleIds: Array.Empty()), - new(nexusModsModId: 2, name: "Demo Mod 2", allowedNexusModsUserIds: Array.Empty(), manuallyLinkedNexusModsUserIds: Array.Empty(), knownModuleIds: Array.Empty(), manuallyLinkedModuleIds: Array.Empty()), - new(nexusModsModId: 3, name: "Demo Mod 3", allowedNexusModsUserIds: Array.Empty(), manuallyLinkedNexusModsUserIds: Array.Empty(), knownModuleIds: Array.Empty(), manuallyLinkedModuleIds: Array.Empty()), - new(nexusModsModId: 4, name: "Demo Mod 4", allowedNexusModsUserIds: Array.Empty(), manuallyLinkedNexusModsUserIds: Array.Empty(), knownModuleIds: Array.Empty(), manuallyLinkedModuleIds: Array.Empty()), + new(nexusModsModId: 1, name: "Demo Mod 1", ownerNexusModsUserIds: Array.Empty(), allowedNexusModsUserIds: Array.Empty(), manuallyLinkedNexusModsUserIds: Array.Empty(), knownModuleIds: Array.Empty(), manuallyLinkedModuleIds: Array.Empty()), + new(nexusModsModId: 2, name: "Demo Mod 2", ownerNexusModsUserIds: Array.Empty(), allowedNexusModsUserIds: Array.Empty(), manuallyLinkedNexusModsUserIds: Array.Empty(), knownModuleIds: Array.Empty(), manuallyLinkedModuleIds: Array.Empty()), + new(nexusModsModId: 3, name: "Demo Mod 3", ownerNexusModsUserIds: Array.Empty(), allowedNexusModsUserIds: Array.Empty(), manuallyLinkedNexusModsUserIds: Array.Empty(), knownModuleIds: Array.Empty(), manuallyLinkedModuleIds: Array.Empty()), + new(nexusModsModId: 4, name: "Demo Mod 4", ownerNexusModsUserIds: Array.Empty(), allowedNexusModsUserIds: Array.Empty(), manuallyLinkedNexusModsUserIds: Array.Empty(), knownModuleIds: Array.Empty(), manuallyLinkedModuleIds: Array.Empty()), }; private static List? _crashReports; public static Task GetProfile() => Task.FromResult(_profile); - public static IAsyncEnumerable GetMods() => _mods.ToAsyncEnumerable(); + public static IAsyncEnumerable GetMods() => _mods.ToAsyncEnumerable(); public static async IAsyncEnumerable GetCrashReports(IHttpClientFactory factory) { static string GetException(ExceptionModel? exception, bool inner = false) => exception is null ? string.Empty : $""" diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Administration/QuartzManager.razor b/src/BUTR.Site.NexusMods.Client/Pages/Administration/QuartzManager.razor index 458c0b69..23828899 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/Administration/QuartzManager.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/Administration/QuartzManager.razor @@ -34,7 +34,7 @@ - + @@ -86,7 +86,7 @@ - + @@ -99,11 +99,11 @@ private string _jobId = string.Empty; - private DataGridPaging? _dataGridRef; + private DataGridVirtual? _dataGridRef; - private async Task.ItemsResponse?> Paging(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct) + private async Task.ItemsResponse?> Paging(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct) { - var response = await _quartzClient.HistoryPaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); + var response = await _quartzClient.JobsPaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); return response is { Value: { } data } ? new(data.Metadata, data.Items, data.AdditionalMetadata) : null; } @@ -111,7 +111,7 @@ { if (string.IsNullOrEmpty(_jobId)) return; - await _quartzClient.TriggerJobAsync(jobId: _jobId); + await _quartzClient.AddTriggerAsync(_jobId); await Task.Delay(1000); diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Administration/RoleManager.razor b/src/BUTR.Site.NexusMods.Client/Pages/Administration/RoleManager.razor index a0866dc5..0b95ac0f 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/Administration/RoleManager.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/Administration/RoleManager.razor @@ -53,7 +53,7 @@ { try { - if (await _userClient.SetRoleAsync(nexusModsUserId: (int) _model.UserId, role: _model.Role) is { Error: not null }) + if (await _userClient.SetRoleAsync(userId: (int) _model.UserId, role: _model.Role) is { Error: not null }) { await _notificationService.Success( $"Assigned '{_model.Role}' to user with id '{_model.UserId}'!", diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Basic/Login.razor b/src/BUTR.Site.NexusMods.Client/Pages/Basic/Login.razor index 85221e0b..b9bf2aa6 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/Basic/Login.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/Basic/Login.razor @@ -45,7 +45,7 @@ TextColor="@TextColor.White" Type="@ButtonType.Link" To="@NexusModsTokenUrl"> - Log In via NexusMods Token + Log In via NexusMods API Key @@ -67,11 +67,11 @@ private string NexusModsSSOUrl => $"login-nexusmods-sso{new Uri(_navigationManager.Uri).Query}"; - private string NexusModsTokenUrl => $"login-nexusmods-token{new Uri(_navigationManager.Uri).Query}"; + private string NexusModsTokenUrl => $"login-nexusmods-apikey{new Uri(_navigationManager.Uri).Query}"; private async void OnDemoLogin() { - if (await _authenticationProvider.AuthenticateAsync("", "demo") is not null) + if (await _authenticationProvider.AuthenticateWithApiKeyAsync("", "demo") is not null) { _navigationManager.NavigateTo(_navigationManager.QueryString("returnUrl") ?? ""); } diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsToken.razor b/src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsApiKey.razor similarity index 93% rename from src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsToken.razor rename to src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsApiKey.razor index 35372413..b85429cb 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsToken.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsApiKey.razor @@ -1,5 +1,5 @@ @attribute [Authorize(Roles = $"{ApplicationRoles.Anonymous}")] -@page "/login-nexusmods-token" +@page "/login-nexusmods-apikey" @inject NavigationManager _navigationManager @inject AuthenticationProvider _authenticationProvider @@ -46,7 +46,7 @@ try { - if (await _authenticationProvider.AuthenticateAsync(_model.Input, "nexusmods") is null) + if (await _authenticationProvider.AuthenticateWithApiKeyAsync(_model.Input, "nexusmods") is null) { _isLoading = false; StateHasChanged(); diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsOAuth2.razor b/src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsOAuth2.razor new file mode 100644 index 00000000..9003a2c7 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsOAuth2.razor @@ -0,0 +1,25 @@ +@attribute [Authorize(Roles = $"{ApplicationRoles.Anonymous}")] +@page "/login-nexusmods-oauth2" + +@inject IAuthenticationClient _authenticationClient; +@inject ILocalStorageService _localStorage; +@inject NavigationManager _navigationManager; + +@code { + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + var response = await _authenticationClient.GetOAuthUrlAsync(); + if (response.Value?.Url is null) + { + _navigationManager.NavigateTo("login"); + return; + } + + await _localStorage.SetItemAsync("nexusmods_state", response.Value.State); + _navigationManager.NavigateTo(response.Value.Url); + } + +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsOAuth2Manual.razor b/src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsOAuth2Manual.razor new file mode 100644 index 00000000..3aa87cd3 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsOAuth2Manual.razor @@ -0,0 +1,66 @@ +@attribute [Authorize(Roles = $"{ApplicationRoles.Anonymous}")] +@page "/login-nexusmods-oauth2-manual" + +@inject IAuthenticationClient _authenticationClient; +@inject ILocalStorageService _localStorage; +@inject NavigationManager _navigationManager; + + + @if (!string.IsNullOrEmpty(Url)) + { +
+ + + NexusMods OAuth2 auth is partially supported. +
+ Please login to NexusMods by clicking the Login button and paste the url after redirect to here +
+ + + + Callback Url + + + +
+
+ } +
+ +@code { + + private string Url = string.Empty; + private string CallbackUrl = string.Empty; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + var response = await _authenticationClient.GetOAuthUrlAsync(); + if (response.Value?.Url is null) + { + _navigationManager.NavigateTo("login"); + return; + } + + Url = response.Value.Url; + await _localStorage.SetItemAsync("nexusmods_state", response.Value.State); + } + + private void Callback() + { + var url = new Uri(CallbackUrl); + _navigationManager.NavigateTo($"oauth-callback{url.Query}"); + } + +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsSSO.razor b/src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsSSO.razor index 7d91bbbc..5c43f3a8 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsSSO.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/Basic/LoginNexusModsSSO.razor @@ -114,7 +114,7 @@ try { - var result = await _authenticationProvider.AuthenticateAsync(response.Data.ApiKey, "nexusmods"); + var result = await _authenticationProvider.AuthenticateWithApiKeyAsync(response.Data.ApiKey, "nexusmods"); if (result is null) { await _notificationService.Error("Failed to authorize!"); diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Basic/OAuthCallback.razor b/src/BUTR.Site.NexusMods.Client/Pages/Basic/OAuthCallback.razor new file mode 100644 index 00000000..193c089d --- /dev/null +++ b/src/BUTR.Site.NexusMods.Client/Pages/Basic/OAuthCallback.razor @@ -0,0 +1,56 @@ +@attribute [Authorize(Roles = $"{ApplicationRoles.Anonymous}")] +@page "/oauth-callback" + +@inject NavigationManager _navigationManager; +@inject AuthenticationProvider _authenticationProvider; +@inject ILocalStorageService _localStorage; +@inject INotificationService _notificationService + +@code { + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + if (!await _localStorage.ContainKeyAsync("nexusmods_state")) + { + await _notificationService.Error("Failed to authenticate! State verification failed", "Error!"); + return; + } + + var queries = _navigationManager.QueryString(); + var queryStatRaw = queries["state"]; + var queryCode = queries["code"]; + + try + { + var state = await _localStorage.GetItemAsync("nexusmods_state"); + if (!Guid.TryParse(queryStatRaw, out var queryState) || state != queryState) + { + await _notificationService.Error("Failed to authenticate! State verification failed", "Error!"); + return; + } + + try + { + if (await _authenticationProvider.AuthenticateWithOAuth2Async(queryCode, queryState, "nexusmods") is null) + { + await _notificationService.Error("Failed to authenticate!", "Error!"); + } + else + { + _navigationManager.NavigateTo(_navigationManager.QueryString("returnUrl") ?? ""); + } + } + catch + { + await _notificationService.Error("Failed to authenticate!", "Error!"); + } + } + finally + { + await _localStorage.RemoveItemAsync("nexusmods_state"); + } + } + +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Basic/Profile.razor b/src/BUTR.Site.NexusMods.Client/Pages/Basic/Profile.razor index 7b929226..54a0a61b 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/Basic/Profile.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/Basic/Profile.razor @@ -194,7 +194,7 @@ else //var base64Token = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(token)); //_tokenUrl = $"{_navigationManager.BaseUri}/{base64Token}"; - var userResponse = await _userClient.ProfileAsync(); + var userResponse = await _userClient.GetProfileAsync(); _user = userResponse.Value; if (_user?.GitHubUserId is not null) diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Discord/OAuthCallback.razor b/src/BUTR.Site.NexusMods.Client/Pages/Discord/OAuthCallback.razor index 2a304115..8a8d9d66 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/Discord/OAuthCallback.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/Discord/OAuthCallback.razor @@ -102,7 +102,7 @@ return; } - await _discordClient.LinkAsync(code: queryCode); + await _discordClient.AddLinkAsync(code: queryCode); _ = await _authenticationProvider.ValidateAsync(); if (await _discordClient.GetUserInfoAsync() is { Value: var userInfo }) diff --git a/src/BUTR.Site.NexusMods.Client/Pages/GOG/OAuthCallback.razor b/src/BUTR.Site.NexusMods.Client/Pages/GOG/OAuthCallback.razor index ebf8cde8..b242d0c6 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/GOG/OAuthCallback.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/GOG/OAuthCallback.razor @@ -61,7 +61,7 @@ var queries = _navigationManager.QueryString(); var queryCode = queries["code"]; - await _gogClient.LinkAsync(code: queryCode); + await _gogClient.AddLinkAsync(code: queryCode); _ = await _authenticationProvider.ValidateAsync(); if (await _gogClient.GetUserInfoAsync() is { Value: var userInfo }) diff --git a/src/BUTR.Site.NexusMods.Client/Pages/GitHub/OAuthCallback.razor b/src/BUTR.Site.NexusMods.Client/Pages/GitHub/OAuthCallback.razor index 379b5cf6..b949eef0 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/GitHub/OAuthCallback.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/GitHub/OAuthCallback.razor @@ -102,7 +102,7 @@ return; } - await _gitHubClient.LinkAsync(code: queryCode); + await _gitHubClient.AddLinkAsync(code: queryCode); _ = await _authenticationProvider.ValidateAsync(); if (await _gitHubClient.GetUserInfoAsync() is { Value: var userInfo }) diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Moderator/AllowUserModuleId.razor b/src/BUTR.Site.NexusMods.Client/Pages/Moderator/AllowUserModuleId.razor index 9876e981..a84e8062 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/Moderator/AllowUserModuleId.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/Moderator/AllowUserModuleId.razor @@ -4,7 +4,7 @@ @inject INotificationService _notificationService @inject INexusModsUserClient _userClient - + Remove Allowed Module Id @@ -17,10 +17,10 @@ Module Id to Remove - @{ - _moduleIdIdToDelete = _dataGridRef.Value.AllowedModuleIds.First(); - foreach (var allowedModId in _dataGridRef.Value.AllowedModuleIds) + _moduleIdToDelete = _dataGridRef.Value.ModuleIds.First(); + foreach (var allowedModId in _dataGridRef.Value.ModuleIds) { @allowedModId } @@ -47,7 +47,7 @@ NexusMods User Url - + @@ -70,11 +70,13 @@ Allowed User Module Ids - + - - - @(string.Join(", ", context.AllowedModuleIds)) + + @($"{context.NexusModsUserId} ({context.NexusModsUsername})") + + + @(string.Join(", ", context.ModuleIds)) @@ -98,19 +100,19 @@ private readonly AllowUserModel _model = new() { UserUrl = string.Empty, ModuleId = string.Empty }; - private string? _moduleIdIdToDelete; + private string? _moduleIdToDelete; private Modal _modalRef = default!; - private DataGridPaging? _dataGridRef; + private DataGridPaging? _dataGridRef; private async Task ShowModal() => await _modalRef.Show(); private async Task HideModal(bool save) { await _modalRef.Hide(); - if (save && _dataGridRef?.Value is not null && _moduleIdIdToDelete is not null) + if (save && _dataGridRef?.Value is not null && _moduleIdToDelete is not null) { - if (await _userClient.ToModuleManualUnlinkAsync(_dataGridRef.Value.NexusModsUserId, _moduleIdIdToDelete) is { Error: not null }) + if (await _userClient.RemoveModuleManualLinkAsync(userId: _dataGridRef.Value.NexusModsUserId, moduleId: _moduleIdToDelete) is { Error: not null }) { await _notificationService.Success($"Disallowed succesfully!", "Success!"); await _dataGridRef.Reload(); @@ -148,13 +150,13 @@ } } - private async Task.ItemsResponse?> GetAllowUserMods(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct) + private async Task.ItemsResponse?> GetAllowUserMods(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct) { - var response = await _userClient.ToModuleManualLinkPaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); + var response = await _userClient.GetModuleManualLinkPaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); return response is { Value: { } data } ? new(data.Metadata, data.Items, data.AdditionalMetadata) : null; } - private async Task OnDisallow(ButtonRowContext context) + private async Task OnDisallow(ButtonRowContext context) { if (context.DeleteCommand.Item is not null && await DoUserDisallowMod(context.DeleteCommand.Item)) { @@ -164,12 +166,17 @@ private async Task DoUserAllowMod(AllowUserModel model) { - if (!NexusModsUtils.TryParse(model.UserUrl, out _, out var nexusModsId) && !uint.TryParse(model.UserUrl, out nexusModsId)) + var hasUsername = NexusModsUtils.TryParseUsername(model.UserUrl, out var username); + var hasUserId = NexusModsUtils.TryParseUserId(model.UserUrl, out _, out var userId); + if (!hasUsername && !hasUserId) + { + await _notificationService.Error("Failed to parse the user url!", "Error!"); return false; + } - return await _userClient.ToModuleManualLinkAsync(nexusModsUserId:(int) nexusModsId, moduleId: model.ModuleId) is { Error: not null }; + return await _userClient.AddModuleManualLinkAsync(moduleId: model.ModuleId, userId: hasUserId ? (int) userId : null, username: hasUsername ? username : null) is { Error: not null }; } - private async Task DoUserDisallowMod(NexusModsUserToModuleManualLinkModel model) + private async Task DoUserDisallowMod(UserManuallyLinkedModuleModel model) { await ShowModal(); return true; diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Moderator/ManualModuleIdLink.razor b/src/BUTR.Site.NexusMods.Client/Pages/Moderator/ManualModuleIdLink.razor index 883a1f58..28e39b11 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/Moderator/ManualModuleIdLink.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/Moderator/ManualModuleIdLink.razor @@ -4,6 +4,40 @@ @inject INotificationService _notificationService @inject INexusModsModClient _modClient + + + + Remove Allowed Module Id + + + + @if (_dataGridRef?.Value is not null) + { +
+ + Module Id to Remove + + + + +
+ } +
+ + + + +
+
+ Manual Module Id Link @@ -41,10 +75,12 @@ Manual Module Id Links - + - - + + + @(string.Join(", ", context.NexusModsMods.Select(x => x.NexusModsModId))) + @@ -65,7 +101,29 @@ private readonly ManualLinkModel _model = new() { ModuleId = string.Empty, NexusModsUrl = string.Empty }; - private DataGridPaging? _dataGridRef; + private int? _modIdToDelete; + + private Modal _modalRef = default!; + private DataGridPaging? _dataGridRef; + + private async Task ShowModal() => await _modalRef.Show(); + + private async Task HideModal(bool save) + { + await _modalRef.Hide(); + if (save && _dataGridRef?.Value is not null && _modIdToDelete is not null) + { + if (await _modClient.RemoveModuleManualLinkAsync(moduleId: _dataGridRef.Value.ModuleId, modId: _modIdToDelete.Value) is { Error: not null }) + { + await _notificationService.Success($"Disallowed succesfully!", "Success!"); + await _dataGridRef.Reload(); + } + else + { + await _notificationService.Error($"Failed to disallow!", "Error!"); + } + } + } private async Task OnSubmit() { @@ -99,29 +157,30 @@ StateHasChanged(); } - private async Task.ItemsResponse?> GetManualLinks(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct) + private async Task.ItemsResponse?> GetManualLinks(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct) { - var response = await _modClient.ToModuleManualLinkPaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); + var response = await _modClient.GetModuleManualLinkPaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); return response is { Value: { } data } ? new(data.Metadata, data.Items, data.AdditionalMetadata) : null; } - private async Task OnDisallow(ButtonRowContext context) + private async Task OnDisallow(ButtonRowContext context) { if (context.DeleteCommand.Item is not null && await DoManualUnlink(context.DeleteCommand.Item)) { - await context.DeleteCommand.Clicked.InvokeAsync(); + //await context.DeleteCommand.Clicked.InvokeAsync(); } } private async Task DoManualLink(ManualLinkModel model) { - if (!NexusModsUtils.TryParse(model.NexusModsUrl, out _, out var nexusModsId) && !uint.TryParse(model.NexusModsUrl, out nexusModsId)) + if (!NexusModsUtils.TryParseModUrl(model.NexusModsUrl, out _, out var nexusModsId) && !uint.TryParse(model.NexusModsUrl, out nexusModsId)) return false; - return await _modClient.ToModuleManualLinkAsync(moduleId: model.ModuleId, nexusModsModId: (int) nexusModsId) is { Error: not null }; + return await _modClient.AddModuleManualLinkAsync(moduleId: model.ModuleId, modId: (int) nexusModsId) is { Error: not null }; } - private async Task DoManualUnlink(NexusModsModToModuleModel model) + private async Task DoManualUnlink(LinkedByStaffModuleNexusModsModsModel model) { - return await _modClient.ToModuleManualUnlinkAsync(moduleId: model.ModuleId) is { Error: not null }; + await ShowModal(); + return true; } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Steam/OpenIdCallback.razor b/src/BUTR.Site.NexusMods.Client/Pages/Steam/OpenIdCallback.razor index cb657679..58fafd16 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/Steam/OpenIdCallback.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/Steam/OpenIdCallback.razor @@ -82,7 +82,7 @@ var queries = _navigationManager.QueryString(); - await _steamClient.LinkAsync(queries: queries.AllKeys.Select(x => new { Key = x, Value = queries[x] }).ToDictionary(x => x.Key!, x => x.Value!)); + await _steamClient.AddLinkAsync(queries: queries.AllKeys.Select(x => new { Key = x, Value = queries[x] }).ToDictionary(x => x.Key!, x => x.Value!)); _ = await _authenticationProvider.ValidateAsync(); if (await _steamClient.GetUserInfoAsync() is { Value: var userInfo }) diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Tools/Articles.razor b/src/BUTR.Site.NexusMods.Client/Pages/Tools/Articles.razor index 6131f212..9249ed25 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/Tools/Articles.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/Tools/Articles.razor @@ -40,7 +40,7 @@ private async Task.ItemsResponse?> GetArticles(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct = default) { - var response = await _articlesClient.PaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); + var response = await _articlesClient.GetPaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); return response is { Value: { } data } ? new(data.Metadata, data.Items, data.AdditionalMetadata) : null; } @@ -50,7 +50,7 @@ await _jsRuntime.InvokeVoidAsync("open", mod.Url(TenantUtils.FromTenantToGameDomain(await _tenantProvider.GetTenantAsync())!), "_blank"); } - private async Task> GetAutocompleteValues(string filter, CancellationToken ct) => (await _articlesClient.AutocompleteAsync(filter, ct)).Value ?? Array.Empty(); + private async Task> GetAutocompleteValues(string filter, CancellationToken ct) => (await _articlesClient.GetAutocompleteAuthorNamesAsync(filter, ct)).Value ?? Array.Empty(); private static IEnumerable GetFilters(IEnumerable columnInfos) { diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Tools/ExposedMods.razor b/src/BUTR.Site.NexusMods.Client/Pages/Tools/ExposedMods.razor index bf5b118b..30a9df37 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/Tools/ExposedMods.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/Tools/ExposedMods.razor @@ -10,11 +10,11 @@ Exposed Mods - + - - - @(string.Join(", ", context.Mods.Select(x => x.ModuleId))) + + + @(string.Join(", ", context.Modules.Select(x => x.ModuleId))) @@ -29,21 +29,21 @@ private enum EntityFields { NexusModsModId, ModuleId, LastUpdateDate } - private DataGridPaging? _dataGridPagingRef; + private DataGridPaging? _dataGridPagingRef; - private async Task.ItemsResponse?> GetExposedMods(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct = default) + private async Task.ItemsResponse?> GetExposedMods(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct = default) { - var response = await _exposedModsClient.PaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); + var response = await _exposedModsClient.GetPaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); return response is { Value: { } data } ? new(data.Metadata, data.Items, data.AdditionalMetadata) : null; } - private async Task OnClick(ExposedNexusModsModModel? mod) + private async Task OnClick(LinkedByExposureNexusModsModModelsModel? mod) { if (mod is not null) await _jsRuntime.InvokeVoidAsync("open", mod.Url(TenantUtils.FromTenantToGameDomain(await _tenantProvider.GetTenantAsync())!), "_blank"); } - private async Task> GetAutocompleteValues(string filter, CancellationToken ct) => (await _exposedModsClient.AutocompleteAsync(new(filter), ct)).Value ?? Array.Empty(); + private async Task> GetAutocompleteValues(string filter, CancellationToken ct) => (await _exposedModsClient.GetAutocompleteModuleIdsAsync(new(filter), ct)).Value ?? Array.Empty(); private static IEnumerable GetFilters(IEnumerable columnInfos) { diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Tools/RecreateStacktrace.razor b/src/BUTR.Site.NexusMods.Client/Pages/Tools/RecreateStacktrace.razor index 586a86f1..5938e030 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/Tools/RecreateStacktrace.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/Tools/RecreateStacktrace.razor @@ -30,7 +30,7 @@ protected override async Task OnInitializedAsync() { - var response = await _recreateStacktraceClient.JsonAsync(Id, CancellationToken.None); + var response = await _recreateStacktraceClient.GetJsonAsync(Id, CancellationToken.None); if (response.Value is null) return; recreatedStacktraces = response.Value; } diff --git a/src/BUTR.Site.NexusMods.Client/Pages/Tools/RecreateStacktracePrerendered.razor b/src/BUTR.Site.NexusMods.Client/Pages/Tools/RecreateStacktracePrerendered.razor index 08f5d89b..9c8461fe 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/Tools/RecreateStacktracePrerendered.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/Tools/RecreateStacktracePrerendered.razor @@ -19,7 +19,7 @@ protected override async Task OnInitializedAsync() { - html = await _recreateStacktraceClient.HtmlAsync(Id, CancellationToken.None); + html = await _recreateStacktraceClient.GetHtmlAsync(Id, CancellationToken.None); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Client/Pages/User/AllowUserMod.razor b/src/BUTR.Site.NexusMods.Client/Pages/User/AllowUserMod.razor index 4fa3ccbc..a43b9c4b 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/User/AllowUserMod.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/User/AllowUserMod.razor @@ -19,10 +19,10 @@ @@ -47,7 +47,7 @@ NexusMods User Url - + @@ -70,11 +70,11 @@ Allowed Mods to Users - + - - - @(string.Join(", ", context.AllowedNexusModsUserIds)) + + + @(string.Join(", ", context.NexusModsUsers.Select(x => $"{x.NexusModsUserId} ({x.NexusModsUsername})"))) @@ -101,7 +101,7 @@ private int? _userIdToDelete; private Modal _modalRef = default!; - private DataGridPaging? _dataGridRef; + private DataGridPaging? _dataGridRef; private async Task ShowModal() => await _modalRef.Show(); @@ -110,7 +110,7 @@ await _modalRef.Hide(); if (save && _dataGridRef?.Value is not null && _userIdToDelete is not null) { - if (await _userClient.ToNexusModsModManualUnlinkAsync(nexusModsUserId: _userIdToDelete, nexusModsModId: _dataGridRef.Value.NexusModsModId, CancellationToken.None) is { Error: not null }) + if (await _userClient.RemoveNexusModsModManualLinkAsync(userId: (int) _userIdToDelete, modId: _dataGridRef.Value.NexusModsModId, cancellationToken: CancellationToken.None) is { Error: not null }) { await _notificationService.Success($"Disallowed succesfully!", "Success!"); await _dataGridRef.Reload(); @@ -148,13 +148,13 @@ } } - private async Task.ItemsResponse?> GetAllowUserMods(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct) + private async Task.ItemsResponse?> GetAllowUserMods(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct) { - var response = await _userClient.ToNexusModsModManualLinkPaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); + var response = await _userClient.GetNexusModsModManualLinkPaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); return response is { Value: { } data } ? new(data.Metadata, data.Items, data.AdditionalMetadata) : null; } - private async Task OnDisallow(ButtonRowContext context) + private async Task OnDisallow(ButtonRowContext context) { if (context.DeleteCommand.Item is not null && await DoUserDisallowMod(context.DeleteCommand.Item)) { @@ -164,15 +164,23 @@ private async Task DoUserAllowMod(AllowUserModel model) { - if (!NexusModsUtils.TryParse(model.UserUrl, out _, out var nexusModsUserId) && !uint.TryParse(model.UserUrl, out nexusModsUserId)) + var hasUsername = NexusModsUtils.TryParseUsername(model.UserUrl, out var username); + var hasUserId = NexusModsUtils.TryParseUserId(model.UserUrl, out _, out var userId); + if (!hasUsername && !hasUserId) + { + await _notificationService.Error("Failed to parse the user url!", "Error!"); return false; - - if (!NexusModsUtils.TryParse(model.ModUrl, out _, out var nexusModsModId) && !uint.TryParse(model.ModUrl, out nexusModsModId)) + } + + if (!NexusModsUtils.TryParseModUrl(model.ModUrl, out _, out var modId)) + { + await _notificationService.Error("Failed to parse the mod url!", "Error!"); return false; + } - return await _userClient.ToNexusModsModManualLinkAsync((int) nexusModsUserId, (int) nexusModsModId) is { Error: not null }; + return await _userClient.AddNexusModsModManualLinkAsync(modId: (int) modId, userId: hasUserId ? (int) userId : null, username: hasUsername ? username : null) is { Error: not null }; } - private async Task DoUserDisallowMod(NexusModsUserToNexusModsModManualLinkModel model) + private async Task DoUserDisallowMod(UserManuallyLinkedModModel model) { await ShowModal(); return true; diff --git a/src/BUTR.Site.NexusMods.Client/Pages/User/CrashReportsViewer.razor b/src/BUTR.Site.NexusMods.Client/Pages/User/CrashReportsViewer.razor index a40bb276..2bba2850 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/User/CrashReportsViewer.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/User/CrashReportsViewer.razor @@ -112,7 +112,7 @@ if (save && _datagridPagingRef?.Value is not null) { var updatedCrashReport = _datagridPagingRef.Value with { }; - if (await _crashReportsClient.UpdateAsync(updatedCrashReport) is { Error: not null }) + if (await _crashReportsClient.UpdateAsync(updatedCrashReport.Id, updatedCrashReport.Status, updatedCrashReport.Comment) is { Error: not null }) { await _notificationService.Success("Saved Crash Report!", "Success!"); } @@ -134,7 +134,7 @@ return await _crashReportsClient.PaginatedStreamingAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), ct); } - private async Task> GetAutocompleteValues(string filter, CancellationToken ct) => await _crashReportsClient.AutocompleteAsync(new(filter), ct) is { Value: { } data } ? data : Array.Empty(); + private async Task> GetAutocompleteValues(string filter, CancellationToken ct) => await _crashReportsClient.GetAutocompleteModuleIdsAsync(new(filter), ct) is { Value: { } data } ? data : Array.Empty(); private IEnumerable GetFilters(IEnumerable columnInfos) { diff --git a/src/BUTR.Site.NexusMods.Client/Pages/User/Mods.razor b/src/BUTR.Site.NexusMods.Client/Pages/User/Mods.razor index 41d5b946..9ecbe200 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/User/Mods.razor +++ b/src/BUTR.Site.NexusMods.Client/Pages/User/Mods.razor @@ -2,7 +2,6 @@ @page "/mods" @inject INexusModsUserClient _userClient -@inject INexusModsModClient _modClient @inject TenantProvider _tenantProvider @inject IJSRuntime _jsRuntime; @@ -33,20 +32,20 @@ Linked Mods - + - - - + + + @(string.Join(", ", context.AllowedNexusModsUserIds)) - + @(string.Join(", ", context.ManuallyLinkedNexusModsUserIds)) - + @(string.Join(", ", context.ManuallyLinkedModuleIds)) - + @(string.Join(", ", context.KnownModuleIds)) @@ -67,10 +66,10 @@ Available Mods - + - - + + @@ -87,8 +86,8 @@ private LinkModModel _model = new(); - private DataGridPaging? _dataGridRef; - private DataGridPaging? _dataGridRef2; + private DataGridPaging? _dataGridRef; + private DataGridPaging? _dataGridRef2; private async Task OnSubmit() { @@ -107,36 +106,36 @@ private async Task DoLinkMod(LinkModModel model) { - if (!NexusModsUtils.TryParse(model.ModUrl, out _, out var modId) && !uint.TryParse(model.ModUrl, out modId)) + if (!NexusModsUtils.TryParseModUrl(model.ModUrl, out _, out var modId) && !uint.TryParse(model.ModUrl, out modId)) return false; - return await _userClient.ToNexusModsModLinkAsync((int) modId) is { Error: not null }; + return await _userClient.AddNexusModsModLinkAsync((int) modId) is { Error: not null }; } - private async Task DoUnlinkMod(NexusModsModModel model) + private async Task DoUnlinkMod(UserLinkedModModel model) { - return await _userClient.ToNexusModsModUnlinkAsync(nexusModsModId: model.NexusModsModId) is { Error: not null }; + return await _userClient.RemoveNexusModsModLinkAsync(modId: model.NexusModsModId) is { Error: not null }; } - private async Task.ItemsResponse?> GetMods(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct = default) + private async Task.ItemsResponse?> GetMods(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct = default) { - var response = await _userClient.ToNexusModsModPaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); + var response = await _userClient.GetNexusModsModsPaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); return response is { Value: { } data } ? new(data.Metadata, data.Items, data.AdditionalMetadata) : null; } - private async Task.ItemsResponse?> GetAllowedMods(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct = default) + private async Task.ItemsResponse?> GetAllowedMods(int page, int pageSize, ICollection filters, ICollection sortings, CancellationToken ct = default) { - var response = await _modClient.AvailablePaginatedAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); + var response = await _userClient.GetNexusModsModsPaginateAvailabledAsync(new(page: page, pageSize: pageSize, filters: filters, sortings: sortings), cancellationToken: ct); return response is { Value: { } data } ? new(data.Metadata, data.Items, data.AdditionalMetadata) : null; } - private async Task OnClick(NexusModsModModel? mod) + private async Task OnClick(UserLinkedModModel? mod) { if (mod is not null) await _jsRuntime.InvokeVoidAsync("open", mod.Url(TenantUtils.FromTenantToGameDomain(await _tenantProvider.GetTenantAsync())!), "_blank"); } - private async Task OnDelete(ButtonRowContext context) + private async Task OnDelete(ButtonRowContext context) { if (context.DeleteCommand.Item is not null && await DoUnlinkMod(context.DeleteCommand.Item)) { diff --git a/src/BUTR.Site.NexusMods.Client/Pages/User/StatisticsExceptionTypes.razor.cs b/src/BUTR.Site.NexusMods.Client/Pages/User/StatisticsExceptionTypes.razor.cs index 14cf9cb6..76fdc9a2 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/User/StatisticsExceptionTypes.razor.cs +++ b/src/BUTR.Site.NexusMods.Client/Pages/User/StatisticsExceptionTypes.razor.cs @@ -35,9 +35,10 @@ protected override async Task OnAfterRenderAsync(bool firstRender) await loadingIndicator.Show(); - var data = (await StatisticsClient.TopExceptionsTypesAsync()).Value ?? Array.Empty(); + var data = (await StatisticsClient.GetTopExceptionsTypesAsync()).Value ?? Array.Empty(); _dataGridRef.Values = data; + await _dataGridRef.Reload(); var labels = data.OrderByDescending(x => x.Percentage).Select(x => x.Type).ToList(); var values = data.OrderByDescending(x => x.Percentage).Select(x => x.Percentage).ToList(); diff --git a/src/BUTR.Site.NexusMods.Client/Pages/User/StatisticsInvolved.razor.cs b/src/BUTR.Site.NexusMods.Client/Pages/User/StatisticsInvolved.razor.cs index 37ab79b8..2bfbaef1 100644 --- a/src/BUTR.Site.NexusMods.Client/Pages/User/StatisticsInvolved.razor.cs +++ b/src/BUTR.Site.NexusMods.Client/Pages/User/StatisticsInvolved.razor.cs @@ -74,7 +74,7 @@ private async Task Refresh() await _lineChart.Clear(); if (_modIds.Count == 0) return; - var data = (await StatisticsClient.InvolvedAsync(_gameVersions, _modIds, Array.Empty())).Value ?? Array.Empty(); + var data = (await StatisticsClient.GetInvolvedModulesAsync(_gameVersions, _modIds, Array.Empty())).Value ?? Array.Empty(); var allGameVersions = data.Select(x => x.GameVersion).ToArray(); var allModIdsWithVersions = data @@ -124,14 +124,14 @@ private async Task OnHandleGameVersionReadData(AutocompleteReadDataEventArgs aut { if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested) { - _gameVersionsAutocompleteValues = (await StatisticsClient.AutocompleteGameVersionAsync(autocompleteReadDataEventArgs.SearchValue)).Value ?? Array.Empty(); + _gameVersionsAutocompleteValues = (await StatisticsClient.GetAutocompleteGameVersionsAsync(autocompleteReadDataEventArgs.SearchValue)).Value ?? Array.Empty(); } } private async Task OnHandleModIdReadData(AutocompleteReadDataEventArgs autocompleteReadDataEventArgs) { if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested && autocompleteReadDataEventArgs.SearchValue.Length >= 3) { - _modIdsAutocompleteValues = (await StatisticsClient.AutocompleteModuleIdAsync(autocompleteReadDataEventArgs.SearchValue)).Value ?? Array.Empty(); + _modIdsAutocompleteValues = (await StatisticsClient.GetAutocompleteModuleIdsAsync(autocompleteReadDataEventArgs.SearchValue)).Value ?? Array.Empty(); } } diff --git a/src/BUTR.Site.NexusMods.Client/Services/AuthenticationProvider.cs b/src/BUTR.Site.NexusMods.Client/Services/AuthenticationProvider.cs index 36e629a6..13f8913f 100644 --- a/src/BUTR.Site.NexusMods.Client/Services/AuthenticationProvider.cs +++ b/src/BUTR.Site.NexusMods.Client/Services/AuthenticationProvider.cs @@ -14,11 +14,11 @@ public sealed class AuthenticationProvider public AuthenticationProvider(IAuthenticationClient client, ITokenContainer tokenContainer) { - _authenticationClient = client ?? throw new ArgumentNullException(nameof(client)); - _tokenContainer = tokenContainer ?? throw new ArgumentNullException(nameof(tokenContainer)); + _authenticationClient = client; + _tokenContainer = tokenContainer; } - public async Task AuthenticateAsync(string apiKey, string type, CancellationToken ct = default) + public async Task AuthenticateWithApiKeyAsync(string apiKey, string type, CancellationToken ct = default) { if (type.Equals("demo", StringComparison.OrdinalIgnoreCase)) { @@ -28,7 +28,29 @@ public AuthenticationProvider(IAuthenticationClient client, ITokenContainer toke try { - var response = await _authenticationClient.AuthenticateAsync(apiKey, ct); + var response = await _authenticationClient.AuthenticateWithApiKeyAsync(apiKey, ct); + if (response.Value is null) return null; + + await _tokenContainer.SetTokenAsync(new Token(type, response.Value.Token), ct); + + return response.Value.Token; + } + catch (Exception e) + { + return null; + } + } + public async Task AuthenticateWithOAuth2Async(string code, Guid state, string type, CancellationToken ct = default) + { + if (type.Equals("demo", StringComparison.OrdinalIgnoreCase)) + { + await _tokenContainer.SetTokenAsync(new Token(type, string.Empty), ct); + return string.Empty; + } + + try + { + var response = await _authenticationClient.AuthenticateWithOAuth2Async(code, state, ct); if (response.Value is null) return null; await _tokenContainer.SetTokenAsync(new Token(type, response.Value.Token), ct); diff --git a/src/BUTR.Site.NexusMods.Client/Services/CrashReportsClientWithDemo.cs b/src/BUTR.Site.NexusMods.Client/Services/CrashReportsClientWithDemo.cs index 07887e45..01aab08e 100644 --- a/src/BUTR.Site.NexusMods.Client/Services/CrashReportsClientWithDemo.cs +++ b/src/BUTR.Site.NexusMods.Client/Services/CrashReportsClientWithDemo.cs @@ -36,8 +36,8 @@ public sealed class CrashReportsClientWithDemo : ICrashReportsClient public CrashReportsClientWithDemo(IServiceProvider serviceProvider, ITokenContainer tokenContainer, IHttpClientFactory httpClientFactory) { _implementation = Program.ConfigureClient(serviceProvider, (http, opt) => new CrashReportsClientWithStreaming(http, opt)); - _tokenContainer = tokenContainer ?? throw new ArgumentNullException(nameof(tokenContainer)); - _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + _tokenContainer = tokenContainer; + _httpClientFactory = httpClientFactory; } public async Task> PaginatedStreamingAsync(PaginatedQuery body, CancellationToken ct = default) @@ -49,14 +49,11 @@ public async Task> PaginatedStreamingAsyn return PagingStreamingData.Create(new PagingMetadata(1, (int) Math.Ceiling((double) crashReports.Count / body.PageSize), body.PageSize, crashReports.Count), crashReports.ToAsyncEnumerable(), PagingAdditionalMetadata.Empty); } - return await _implementation.PaginatedStreamingAsync(new PaginatedQuery(body.Page, body.PageSize, body.Filters, body.Sortings), ct); + return await _implementation.PaginatedStreamingAsync(body, ct); } - public async Task PaginatedAsync(PaginatedQuery? body, CancellationToken ct) + public async Task GetPaginatedAsync(PaginatedQuery body, CancellationToken ct) { - if (body is null) - return await _implementation.PaginatedAsync(body, ct); - var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) { @@ -64,22 +61,22 @@ public async Task PaginatedAsync(Pagi return new CrashReportModel2PagingDataApiResultModel(new CrashReportModel2PagingData(PagingAdditionalMetadata.Empty, crashReports, new PagingMetadata(1, (int) Math.Ceiling((double) crashReports.Count / body.PageSize), body.PageSize, crashReports.Count)), null!); } - return await _implementation.PaginatedAsync(new PaginatedQuery(body.Page, body.PageSize, body.Filters, body.Sortings), ct); + return await _implementation.GetPaginatedAsync(body, ct); } - public async Task AutocompleteAsync(string? modId, CancellationToken ct) + public async Task GetAutocompleteModuleIdsAsync(string modId, CancellationToken ct) { var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) { var crashReports = await DemoUser.GetCrashReports(_httpClientFactory).ToListAsync(ct); - return new StringIQueryableApiResultModel(crashReports.SelectMany(x => x.InvolvedModules).Where(x => x.StartsWith(modId ?? string.Empty)).ToArray(), null!); + return new StringIListApiResultModel(crashReports.SelectMany(x => x.InvolvedModules).Where(x => x.StartsWith(modId ?? string.Empty)).ToArray(), null!); } - return new StringIQueryableApiResultModel((await _implementation.AutocompleteAsync(modId, ct)).Value ?? Array.Empty(), null!); + return new StringIListApiResultModel((await _implementation.GetAutocompleteModuleIdsAsync(modId, ct)).Value ?? Array.Empty(), null!); } - public async Task UpdateAsync(CrashReportModel2? body, CancellationToken ct) + public async Task UpdateAsync(Guid crash_report_id, CrashReportStatus? status = null, string? comment = null, CancellationToken ct = default) { var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) @@ -87,6 +84,6 @@ public async Task UpdateAsync(CrashReportModel2? body, Can return new StringApiResultModel("demo", null!); } - return await _implementation.UpdateAsync(body, ct); + return await _implementation.UpdateAsync(crash_report_id, status, comment, ct); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Client/Services/HttpClient/AssetsDelegatingHandler.cs b/src/BUTR.Site.NexusMods.Client/Services/HttpClient/AssetsDelegatingHandler.cs index 36d13724..46da1c76 100644 --- a/src/BUTR.Site.NexusMods.Client/Services/HttpClient/AssetsDelegatingHandler.cs +++ b/src/BUTR.Site.NexusMods.Client/Services/HttpClient/AssetsDelegatingHandler.cs @@ -23,7 +23,7 @@ public DecompressedContent(HttpContent content, Stream stream) : base(stream) public AssetsDelegatingHandler(BrotliDecompressorService brotliDecompressorService) { - _brotliDecompressorService = brotliDecompressorService ?? throw new ArgumentNullException(nameof(brotliDecompressorService)); + _brotliDecompressorService = brotliDecompressorService; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) diff --git a/src/BUTR.Site.NexusMods.Client/Services/HttpClient/AuthenticationInjectionDelegatingHandler.cs b/src/BUTR.Site.NexusMods.Client/Services/HttpClient/AuthenticationInjectionDelegatingHandler.cs index 36f6ebc9..bc1374be 100644 --- a/src/BUTR.Site.NexusMods.Client/Services/HttpClient/AuthenticationInjectionDelegatingHandler.cs +++ b/src/BUTR.Site.NexusMods.Client/Services/HttpClient/AuthenticationInjectionDelegatingHandler.cs @@ -15,8 +15,8 @@ public class AuthenticationInjectionDelegatingHandler : DelegatingHandler public AuthenticationInjectionDelegatingHandler(ITokenContainer tokenContainer, INotificationService notificationService) { - _tokenContainer = tokenContainer ?? throw new ArgumentNullException(nameof(tokenContainer)); - _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); + _tokenContainer = tokenContainer; + _notificationService = notificationService; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) diff --git a/src/BUTR.Site.NexusMods.Client/Services/HttpClient/ICrashReporterClient.cs b/src/BUTR.Site.NexusMods.Client/Services/HttpClient/ICrashReporterClient.cs index 4debb9af..91ac7dde 100644 --- a/src/BUTR.Site.NexusMods.Client/Services/HttpClient/ICrashReporterClient.cs +++ b/src/BUTR.Site.NexusMods.Client/Services/HttpClient/ICrashReporterClient.cs @@ -22,8 +22,8 @@ public sealed class CrashReporterClient : ICrashReporterClient public CrashReporterClient(HttpClient httpClient, IOptions jsonSerializerOptions) { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - _jsonSerializerOptions = jsonSerializerOptions.Value ?? throw new ArgumentNullException(nameof(jsonSerializerOptions)); + _httpClient = httpClient; + _jsonSerializerOptions = jsonSerializerOptions.Value; } public async Task GetCrashReportModelAsync(string id, CancellationToken ct) diff --git a/src/BUTR.Site.NexusMods.Client/Services/NexusModsModClientWithDemo.cs b/src/BUTR.Site.NexusMods.Client/Services/NexusModsModClientWithDemo.cs index 53bfac59..120e5633 100644 --- a/src/BUTR.Site.NexusMods.Client/Services/NexusModsModClientWithDemo.cs +++ b/src/BUTR.Site.NexusMods.Client/Services/NexusModsModClientWithDemo.cs @@ -14,31 +14,26 @@ public sealed class NexusModsModClientWithDemo : INexusModsModClient public NexusModsModClientWithDemo(IServiceProvider serviceProvider, ITokenContainer tokenContainer) { _implementation = Program.ConfigureClient(serviceProvider, (http, opt) => new NexusModsModClient(http, opt)); - _tokenContainer = tokenContainer ?? throw new ArgumentNullException(nameof(tokenContainer)); + _tokenContainer = tokenContainer; } - public async Task RawAsync(string gameDomain, int modId, CancellationToken ct = default) + public async Task GetRawAsync(int mod_id, CancellationToken ct = default) { - return await _implementation.RawAsync(gameDomain, modId, ct); + return await _implementation.GetRawAsync(mod_id, ct); } - public async Task ToModuleManualLinkAsync(string? moduleId = null, int? nexusModsModId = null, CancellationToken ct = default) + public async Task AddModuleManualLinkAsync(int modId, string moduleId, CancellationToken ct = default) { - return await _implementation.ToModuleManualLinkAsync(moduleId, nexusModsModId, ct); + return await _implementation.AddModuleManualLinkAsync(modId, moduleId, ct); } - public async Task ToModuleManualUnlinkAsync(string? moduleId = null, int? nexusModsModId = null, CancellationToken ct = default) + public async Task RemoveModuleManualLinkAsync(int modId, string moduleId, CancellationToken ct = default) { - return await _implementation.ToModuleManualUnlinkAsync(moduleId, nexusModsModId, ct); + return await _implementation.RemoveModuleManualLinkAsync(modId, moduleId, ct); } - public async Task ToModuleManualLinkPaginatedAsync(PaginatedQuery? body = null, CancellationToken ct = default) + public async Task GetModuleManualLinkPaginatedAsync(PaginatedQuery body, CancellationToken ct = default) { - return await _implementation.ToModuleManualLinkPaginatedAsync(body, ct); - } - - public async Task AvailablePaginatedAsync(PaginatedQuery? body = null, CancellationToken ct = default) - { - return await _implementation.AvailablePaginatedAsync(body, ct); + return await _implementation.GetModuleManualLinkPaginatedAsync(body, ct); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Client/Services/NexusModsUserClientWithDemo.cs b/src/BUTR.Site.NexusMods.Client/Services/NexusModsUserClientWithDemo.cs index 802c0b58..2bdb9160 100644 --- a/src/BUTR.Site.NexusMods.Client/Services/NexusModsUserClientWithDemo.cs +++ b/src/BUTR.Site.NexusMods.Client/Services/NexusModsUserClientWithDemo.cs @@ -18,11 +18,11 @@ public sealed class NexusModsUserClientWithDemo : INexusModsUserClient public NexusModsUserClientWithDemo(IServiceProvider serviceProvider, ITokenContainer tokenContainer, StorageCache cache) { _implementation = Program.ConfigureClient(serviceProvider, (http, opt) => new NexusModsUserClient(http, opt)); - _tokenContainer = tokenContainer ?? throw new ArgumentNullException(nameof(tokenContainer)); - _cache = cache ?? throw new ArgumentNullException(nameof(cache)); + _tokenContainer = tokenContainer; + _cache = cache; } - public async Task ProfileAsync(CancellationToken ct) + public async Task GetProfileAsync(CancellationToken ct) { var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) @@ -32,7 +32,7 @@ public async Task ProfileAsync(CancellationToken ct) { try { - var profile = (await _implementation.ProfileAsync(ct)).Value; + var profile = (await _implementation.GetProfileAsync(ct)).Value; return new(new(profile, null!), new CacheOptions { AbsoluteExpiration = DateTimeOffset.UtcNow.AddSeconds(5), @@ -48,67 +48,64 @@ public async Task ProfileAsync(CancellationToken ct) return await _cache.GetAsync("profile", Factory, ct) ?? new ProfileModelApiResultModel(await DemoUser.GetProfile(), null!); } - public async Task SetRoleAsync(int? userId, string? role, CancellationToken ct) + public async Task SetRoleAsync(string role, int? userId = null, string? username = null, CancellationToken ct = default) { var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) return new StringApiResultModel("demo", null!); - return await _implementation.SetRoleAsync(userId, role, ct); + return await _implementation.SetRoleAsync(role, userId, username, ct); } - public async Task RemoveRoleAsync(int? userId, CancellationToken ct) + public async Task RemoveRoleAsync(int? userId = null, string? username = null, CancellationToken ct = default) { var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) return new StringApiResultModel("demo", null!); - return await _implementation.RemoveRoleAsync(userId, ct); + return await _implementation.RemoveRoleAsync(userId, username, ct); } - public async Task ToNexusModsModPaginatedAsync(PaginatedQuery? body = null, CancellationToken ct = default) + public async Task GetNexusModsModsPaginatedAsync(PaginatedQuery body, CancellationToken ct = default) { - if (body is null) - return await _implementation.ToNexusModsModPaginatedAsync(body, ct); - var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) { var mods = await DemoUser.GetMods().ToListAsync(ct); - return new NexusModsModModelPagingDataApiResultModel(new NexusModsModModelPagingData(PagingAdditionalMetadata.Empty, mods, new PagingMetadata(1, (int) Math.Ceiling((double) mods.Count / (double) body.PageSize), body.PageSize, mods.Count)), null!); + return new UserLinkedModModelPagingDataApiResultModel(new UserLinkedModModelPagingData(PagingAdditionalMetadata.Empty, mods, new PagingMetadata(1, (int) Math.Ceiling((double) mods.Count / (double) body.PageSize), body.PageSize, mods.Count)), null!); } - return await _implementation.ToNexusModsModPaginatedAsync(new PaginatedQuery(body.Page, body.PageSize, Array.Empty(), Array.Empty()), ct); + return await _implementation.GetNexusModsModsPaginatedAsync(body, ct); } - public async Task ToNexusModsModUpdateAsync(NexusModsUserToNexusModsModQuery? body = null, CancellationToken ct = default) => - await _implementation.ToNexusModsModUpdateAsync(body, ct); + public async Task UpdateNexusModsModLinkAsync(int modId, int? userId = null, string? username = null, CancellationToken ct = default) => + await _implementation.UpdateNexusModsModLinkAsync(modId, userId, username, ct); - public async Task ToNexusModsModLinkAsync(int? nexusModsModId = null, CancellationToken ct = default) + public async Task AddNexusModsModLinkAsync(int modId, int? userId = null, string? username = null, CancellationToken ct = default) { var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) { var mods = await DemoUser.GetMods().ToListAsync(ct); - if (mods.Find(m => m.NexusModsModId == nexusModsModId) is null) + if (mods.Find(m => m.NexusModsModId == modId) is null) { - mods.Add(new(nexusModsModId ?? 0, $"Demo Mod {nexusModsModId}", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty())); + mods.Add(new(modId, $"Demo Mod {modId}", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty())); return new StringApiResultModel("demo", null!); } return new StringApiResultModel(null, null!); } - return await _implementation.ToNexusModsModLinkAsync(nexusModsModId, ct); + return await _implementation.AddNexusModsModLinkAsync(modId, userId, username, ct); } - public async Task ToNexusModsModUnlinkAsync(int? nexusModsModId = null, CancellationToken ct = default) + public async Task RemoveNexusModsModLinkAsync(int modId, int? userId = null, string? username = null, CancellationToken ct = default) { var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) { var mods = await DemoUser.GetMods().ToListAsync(ct); - if (mods.Find(m => m.NexusModsModId == nexusModsModId) is { } mod) + if (mods.Find(m => m.NexusModsModId == modId) is { } mod) { mods.Remove(mod); return new StringApiResultModel("demo", null!); @@ -117,66 +114,68 @@ public async Task ToNexusModsModUnlinkAsync(int? nexusMods return new StringApiResultModel(null!, null); } - return await _implementation.ToNexusModsModUnlinkAsync(nexusModsModId, ct); + return await _implementation.RemoveNexusModsModLinkAsync(modId, userId, username, ct); } - public async Task ToModuleManualLinkAsync(int? nexusModsUserId = null, string? moduleId = null, CancellationToken ct = default) + public async Task AddModuleManualLinkAsync(string moduleId, int? userId = null, string? username = null, CancellationToken ct = default) { var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) return new StringApiResultModel("demo", null!); - return await _implementation.ToModuleManualLinkAsync(nexusModsUserId, moduleId, ct); + return await _implementation.AddModuleManualLinkAsync(moduleId, userId, username, ct); } - public async Task ToModuleManualUnlinkAsync(int? nexusModsUserId = null, string? moduleId = null, CancellationToken ct = default) + public async Task RemoveModuleManualLinkAsync(string moduleId, int? userId = null, string? username = null, CancellationToken ct = default) { var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) return new StringApiResultModel("demo", null!); - return await _implementation.ToModuleManualUnlinkAsync(nexusModsUserId, moduleId, ct); + return await _implementation.RemoveModuleManualLinkAsync(moduleId, userId, username, ct); } - public async Task ToModuleManualLinkPaginatedAsync(PaginatedQuery? body = null, CancellationToken ct = default) + public async Task GetModuleManualLinkPaginatedAsync(PaginatedQuery body, CancellationToken ct = default) { - if (body is null) - return await _implementation.ToModuleManualLinkPaginatedAsync(body, ct); - var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) - return new NexusModsUserToModuleManualLinkModelPagingDataApiResultModel(new NexusModsUserToModuleManualLinkModelPagingData(PagingAdditionalMetadata.Empty, new List(), new PagingMetadata(1, 1, body.PageSize, 1)), null!); + return new UserManuallyLinkedModuleModelPagingDataApiResultModel(new UserManuallyLinkedModuleModelPagingData(PagingAdditionalMetadata.Empty, new List(), new PagingMetadata(1, 1, body.PageSize, 1)), null!); - return await _implementation.ToModuleManualLinkPaginatedAsync(new PaginatedQuery(body.Page, body.PageSize, Array.Empty(), Array.Empty()), ct); + return await _implementation.GetModuleManualLinkPaginatedAsync(body, ct); } - public async Task ToNexusModsModManualLinkAsync(int? userId = null, int? nexusModsModId = null, CancellationToken ct = default) + public async Task AddNexusModsModManualLinkAsync(int modId, int? userId = null, string? username = null, CancellationToken ct = default) { var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) return new StringApiResultModel("demo", null!); - return await _implementation.ToNexusModsModManualLinkAsync(userId, nexusModsModId, ct); + return await _implementation.AddNexusModsModManualLinkAsync(modId, userId, username, ct); } - public async Task ToNexusModsModManualUnlinkAsync(int? userId = null, int? nexusModsModId = null, CancellationToken ct = default) + public async Task RemoveNexusModsModManualLinkAsync(int modId, int? userId = null, string? username = null, CancellationToken ct = default) { var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) return new StringApiResultModel("demo", null!); - return await _implementation.ToNexusModsModManualUnlinkAsync(userId, nexusModsModId, ct); + return await _implementation.RemoveNexusModsModManualLinkAsync(modId, userId, username, ct); } - public async Task ToNexusModsModManualLinkPaginatedAsync(PaginatedQuery? body = null, CancellationToken ct = default) + public async Task GetNexusModsModManualLinkPaginatedAsync(PaginatedQuery? body = null, CancellationToken ct = default) { if (body is null) - return await _implementation.ToNexusModsModManualLinkPaginatedAsync(body, ct); + return await _implementation.GetNexusModsModManualLinkPaginatedAsync(body, ct); var token = await _tokenContainer.GetTokenAsync(ct); if (token?.Type.Equals("demo", StringComparison.OrdinalIgnoreCase) == true) - return new NexusModsUserToNexusModsModManualLinkModelPagingDataApiResultModel(new NexusModsUserToNexusModsModManualLinkModelPagingData(PagingAdditionalMetadata.Empty, new List(), new PagingMetadata(1, 1, body.PageSize, 1)), null!); + return new UserManuallyLinkedModModelPagingDataApiResultModel(new UserManuallyLinkedModModelPagingData(PagingAdditionalMetadata.Empty, new List(), new PagingMetadata(1, 1, body.PageSize, 1)), null!); - return await _implementation.ToNexusModsModManualLinkPaginatedAsync(body, ct); + return await _implementation.GetNexusModsModManualLinkPaginatedAsync(body, ct); + } + + public async Task GetNexusModsModsPaginateAvailabledAsync(PaginatedQuery body, CancellationToken ct = default) + { + return await _implementation.GetNexusModsModsPaginateAvailabledAsync(body, ct); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Client/Services/SimpleAuthenticationStateProvider.cs b/src/BUTR.Site.NexusMods.Client/Services/SimpleAuthenticationStateProvider.cs index f2b6b9bf..161866d4 100644 --- a/src/BUTR.Site.NexusMods.Client/Services/SimpleAuthenticationStateProvider.cs +++ b/src/BUTR.Site.NexusMods.Client/Services/SimpleAuthenticationStateProvider.cs @@ -21,8 +21,8 @@ public sealed class SimpleAuthenticationStateProvider : AuthenticationStateProvi public SimpleAuthenticationStateProvider(ITokenContainer tokenContainer, AuthenticationProvider authenticationProvider) { - _tokenContainer = tokenContainer ?? throw new ArgumentNullException(nameof(tokenContainer)); - _authenticationProvider = authenticationProvider ?? throw new ArgumentNullException(nameof(authenticationProvider)); + _tokenContainer = tokenContainer; + _authenticationProvider = authenticationProvider; _tokenContainer.OnTokenChanged += ResetAuthenticationState; _task = GetAuthenticationStateInternalAsync(); } diff --git a/src/BUTR.Site.NexusMods.Client/Services/Utils/LocalStorageTokenContainer.cs b/src/BUTR.Site.NexusMods.Client/Services/Utils/LocalStorageTokenContainer.cs index 6cbe193e..43166bbc 100644 --- a/src/BUTR.Site.NexusMods.Client/Services/Utils/LocalStorageTokenContainer.cs +++ b/src/BUTR.Site.NexusMods.Client/Services/Utils/LocalStorageTokenContainer.cs @@ -14,7 +14,7 @@ public sealed class LocalStorageTokenContainer : ITokenContainer public LocalStorageTokenContainer(ILocalStorageService localStorage) { - _localStorage = localStorage ?? throw new ArgumentNullException(nameof(localStorage)); + _localStorage = localStorage; } public async Task GetTokenAsync(CancellationToken ct = default) diff --git a/src/BUTR.Site.NexusMods.Client/Services/Utils/StorageCache.cs b/src/BUTR.Site.NexusMods.Client/Services/Utils/StorageCache.cs index ce17b65d..f0a902f8 100644 --- a/src/BUTR.Site.NexusMods.Client/Services/Utils/StorageCache.cs +++ b/src/BUTR.Site.NexusMods.Client/Services/Utils/StorageCache.cs @@ -36,8 +36,8 @@ private record EntryOptions public StorageCache(ISessionStorageService storage, IOptions jsonSerializerOptions) { - _storage = storage ?? throw new ArgumentNullException(nameof(storage)); - _jsonSerializerOptions = jsonSerializerOptions.Value ?? throw new ArgumentNullException(nameof(jsonSerializerOptions)); + _storage = storage; + _jsonSerializerOptions = jsonSerializerOptions.Value; } public async Task GetAsync(string key_, Func> factory, CancellationToken ct) where T : class diff --git a/src/BUTR.Site.NexusMods.Client/Shared/Header.razor b/src/BUTR.Site.NexusMods.Client/Shared/Header.razor index 9772157b..1ba7313e 100644 --- a/src/BUTR.Site.NexusMods.Client/Shared/Header.razor +++ b/src/BUTR.Site.NexusMods.Client/Shared/Header.razor @@ -9,7 +9,7 @@ Breakpoint="@Breakpoint.Desktop" Background="@Background.Dark" ThemeContrast="@ThemeContrast.Dark"> - +
@@ -19,7 +19,7 @@ - + @@ -207,7 +207,7 @@ { if (state.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Role) is { } claim && claim.Value != ApplicationRoles.Anonymous) { - var response = await _userClient.ProfileAsync(); + var response = await _userClient.GetProfileAsync(); _user = response.Value; StateHasChanged(); } diff --git a/src/BUTR.Site.NexusMods.DependencyInjection/Attributes/ScopedServiceAttribute.cs b/src/BUTR.Site.NexusMods.DependencyInjection/Attributes/ScopedServiceAttribute.cs index 24aab985..7cea7a4c 100644 --- a/src/BUTR.Site.NexusMods.DependencyInjection/Attributes/ScopedServiceAttribute.cs +++ b/src/BUTR.Site.NexusMods.DependencyInjection/Attributes/ScopedServiceAttribute.cs @@ -1,4 +1,7 @@ namespace BUTR.Site.NexusMods.DependencyInjection; [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public sealed class ScopedServiceAttribute : Attribute, IToRegister; \ No newline at end of file +public sealed class ScopedServiceAttribute : Attribute, IToRegister; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public sealed class ScopedServiceAttribute : Attribute, IToRegister; \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/BUTR.Site.NexusMods.Server.ValueObjects.Vogen.csproj b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/BUTR.Site.NexusMods.Server.ValueObjects.Vogen.csproj index 7be481a3..6a2501a0 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/BUTR.Site.NexusMods.Server.ValueObjects.Vogen.csproj +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/BUTR.Site.NexusMods.Server.ValueObjects.Vogen.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Extensions/EfCoreExtensions.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Extensions/EfCoreExtensions.cs deleted file mode 100644 index 4233a0f7..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Extensions/EfCoreExtensions.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.Models; - -public static class EfCoreExtensions -{ - private static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) - where TVogen : struct, IVogen, IEquatable, IEquatable, IComparable, IComparable - where TValueObject : notnull => - propertyBuilder.HasConversion, VogenValueComparer>(); - - private static PropertyBuilder HasVogenConversionNullable(this PropertyBuilder propertyBuilder) - where TVogen : struct, IVogen, IEquatable, IEquatable, IComparable, IComparable - where TValueObject : notnull => - propertyBuilder.HasConversion, VogenValueComparer>(); - - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasValueObjectConversion(); - - - public static PropertyBuilder HasValueObjectConversion(this PropertyBuilder propertyBuilder) => - propertyBuilder.HasVogenConversionNullable(); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Extensions/OpenApiExtensions.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Extensions/OpenApiExtensions.cs deleted file mode 100644 index d8575e74..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Extensions/OpenApiExtensions.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.Models; - -public static class OpenApiExtensions -{ - public static void ValueObjectFilter(this SwaggerGenOptions opt) - { - opt.SchemaFilter(); - } -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Extensions/StringExtensions.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Extensions/StringExtensions.cs index 25f54964..41606d24 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Extensions/StringExtensions.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Extensions/StringExtensions.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.Contracts; - namespace BUTR.Site.NexusMods.Server.Models; internal static class StringExtensions diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ApplicationRole.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ApplicationRole.cs index 3ec441ee..b5365255 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ApplicationRole.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ApplicationRole.cs @@ -3,10 +3,8 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = ApplicationRole; using TValueType = String; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None, deserializationStrictness: DeserializationStrictness.AllowKnownInstances)] -public readonly partial record struct ApplicationRole : IVogen, IHasDefaultValue +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter, deserializationStrictness: DeserializationStrictness.AllowKnownInstances)] +public partial struct ApplicationRole : IVogen, IHasDefaultValue { public static readonly TType Anonymous = From(ApplicationRoles.Anonymous); public static readonly TType User = From(ApplicationRoles.User); @@ -16,8 +14,6 @@ namespace BUTR.Site.NexusMods.Server.Models; public static TType DefaultValue => Anonymous; public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); public static int GetHashCode(TType instance) => VogenDefaults.GetHashCode(instance); @@ -25,5 +21,4 @@ namespace BUTR.Site.NexusMods.Server.Models; public static bool Equals(TType left, TType right, IEqualityComparer comparer) => VogenDefaults.Equals(left, right, comparer); public static int CompareTo(TType left, TType right) => VogenDefaults.CompareTo(left, right); - } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportFileId.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportFileId.cs index cdc4387a..68e1e038 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportFileId.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportFileId.cs @@ -3,14 +3,11 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = CrashReportFileId; using TValueType = String; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct CrashReportFileId : IVogen +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct CrashReportFileId : IVogen { public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); + public static TType DeserializeDangerous(TValueType instance) => __Deserialize(instance); public static int GetHashCode(TType instance) => VogenDefaults.GetHashCode(instance); diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportId.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportId.cs index 408c50ad..a0087f98 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportId.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportId.cs @@ -3,15 +3,12 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = CrashReportId; using TValueType = Guid; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct CrashReportId : IVogen, IVogenSpanParsable, IHasRandomValueGenerator +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct CrashReportId : IVogen, IHasRandomValueGenerator { public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); - public static TType NewRandomValue(Random? random) => From(Guid.NewGuid()); + public static TType DeserializeDangerous(TValueType instance) => __Deserialize(instance); + public static TType NewRandomValue(Random? random) => From(TValueType.NewGuid()); public static int GetHashCode(TType instance) => VogenDefaults.GetHashCode(instance); diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportUrl.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportUrl.cs index 604ec10e..c7d34bad 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportUrl.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportUrl.cs @@ -3,14 +3,11 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = CrashReportUrl; using TValueType = String; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct CrashReportUrl : IVogen +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct CrashReportUrl : IVogen { public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); + public static TType DeserializeDangerous(TValueType instance) => __Deserialize(instance); public static TType From(Uri uri) => From(uri.ToString()); diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportVersion.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportVersion.cs index b25ee4d7..48d0b28a 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportVersion.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/CrashReportVersion.cs @@ -3,14 +3,11 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = CrashReportVersion; using TValueType = Byte; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct CrashReportVersion : IVogen, IVogenParsable, IVogenSpanParsable, IVogenUtf8SpanParsable +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct CrashReportVersion : IVogen { public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); + public static TType DeserializeDangerous(TValueType instance) => __Deserialize(instance); public static int GetHashCode(TType instance) => VogenDefaults.GetHashCode(instance); diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ExceptionTypeId.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ExceptionTypeId.cs index 48bf288c..76a0fd00 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ExceptionTypeId.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ExceptionTypeId.cs @@ -5,14 +5,11 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = ExceptionTypeId; using TValueType = String; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct ExceptionTypeId : IVogen +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct ExceptionTypeId : IVogen { public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); + public static TType DeserializeDangerous(TValueType instance) => __Deserialize(instance); public static TType FromException(ExceptionModel exception) { diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/GameVersion.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/GameVersion.cs index bb512e8b..6d4d5774 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/GameVersion.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/GameVersion.cs @@ -3,14 +3,11 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = GameVersion; using TValueType = System.String; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct GameVersion : IVogen +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct GameVersion : IVogen { public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); + public static TType DeserializeDangerous(TValueType instance) => __Deserialize(instance); public static int GetHashCode(TType instance) => VogenDefaults.GetHashCode(instance); diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ModuleId.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ModuleId.cs index 597cade4..1e08693c 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ModuleId.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ModuleId.cs @@ -3,14 +3,11 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = ModuleId; using TValueType = String; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct ModuleId : IVogen +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct ModuleId : IVogen { public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); + public static TType DeserializeDangerous(TValueType instance) => __Deserialize(instance); public static int GetHashCode(TType instance) => VogenDefaults.GetHashCode(instance); diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ModuleVersion.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ModuleVersion.cs index 3a56d721..6f9b4655 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ModuleVersion.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ModuleVersion.cs @@ -3,14 +3,11 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = ModuleVersion; using TValueType = String; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct ModuleVersion : IVogen +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct ModuleVersion : IVogen { public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); + public static TType DeserializeDangerous(TValueType instance) => __Deserialize(instance); public static int GetHashCode(TType instance) => VogenDefaults.GetHashCode(instance); diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsApiKey.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsApiKey.cs index 5bb6698a..01a8ed9c 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsApiKey.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsApiKey.cs @@ -3,18 +3,15 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = NexusModsApiKey; using TValueType = String; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct NexusModsApiKey : IVogen, IHasDefaultValue +[ValueObject(conversions: Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct NexusModsApiKey : IVogen, IHasDefaultValue { public static readonly TType None = From(string.Empty); public static NexusModsApiKey DefaultValue => None; public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); + public static TType DeserializeDangerous(TValueType instance) => __Deserialize(instance); public static int GetHashCode(TType instance) => VogenDefaults.GetHashCode(instance); diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsArticleId.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsArticleId.cs index 7c8f7382..2596fb60 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsArticleId.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsArticleId.cs @@ -3,18 +3,14 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = NexusModsArticleId; using TValueType = Int32; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct NexusModsArticleId : IVogen, IVogenParsable, IVogenSpanParsable, IVogenUtf8SpanParsable, IHasDefaultValue +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct NexusModsArticleId : IVogen, IHasDefaultValue { public static readonly TType None = From(0); public static TType DefaultValue => None; public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); public static bool TryParseUrl(string urlRaw, out TType articleId) { diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsFileId.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsFileId.cs index 6b552ebd..7e9f6e3f 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsFileId.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsFileId.cs @@ -3,14 +3,10 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = NexusModsFileId; using TValueType = Int32; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct NexusModsFileId : IVogen, IVogenParsable, IVogenSpanParsable, IVogenUtf8SpanParsable +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct NexusModsFileId : IVogen { public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); public static int GetHashCode(TType instance) => VogenDefaults.GetHashCode(instance); diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsGameDomain.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsGameDomain.cs index d3e9f483..7a6091eb 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsGameDomain.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsGameDomain.cs @@ -3,15 +3,14 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = NexusModsGameDomain; using TValueType = String; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None, deserializationStrictness: DeserializationStrictness.AllowKnownInstances)] -public readonly partial record struct NexusModsGameDomain : IVogen, IHasDefaultValue +[ValueObject(conversions: Conversions.SystemTextJson | Conversions.TypeConverter, deserializationStrictness: DeserializationStrictness.AllowKnownInstances)] +public readonly partial struct NexusModsGameDomain : IVogen, IHasDefaultValue { public static readonly TType None = From(string.Empty); public static readonly TType Bannerlord = From(TenantUtils.BannerlordGameDomain); public static readonly TType Rimworld = From(TenantUtils.RimworldGameDomain); public static readonly TType StardewValley = From(TenantUtils.StardewValleyGameDomain); + public static readonly TType Valheim = From(TenantUtils.ValheimGameDomain); public static TType DefaultValue => None; @@ -22,12 +21,11 @@ public static IEnumerable Values yield return Bannerlord; yield return Rimworld; yield return StardewValley; + yield return Valheim; } } public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); public static bool TryParse(string urlRaw, out TType gameDomain) { diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsModId.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsModId.cs index c890aad0..e0c8bfbc 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsModId.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsModId.cs @@ -3,18 +3,14 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = NexusModsModId; using TValueType = Int32; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct NexusModsModId : IVogen, IVogenParsable, IVogenSpanParsable, IVogenUtf8SpanParsable +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct NexusModsModId : IVogen, IHasDefaultValue { public static readonly TType None = From(0); public static TType DefaultValue => None; public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); public static bool TryParseUrl(string? urlRaw, out TType modId) { diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsUserEMail.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsUserEMail.cs index 0f260ba5..1c7da271 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsUserEMail.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsUserEMail.cs @@ -3,18 +3,14 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = NexusModsUserEMail; using TValueType = String; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct NexusModsUserEMail : IVogen, IHasDefaultValue +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct NexusModsUserEMail : IVogen, IHasDefaultValue { public static readonly TType Empty = From(string.Empty); public static TType DefaultValue => Empty; public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); public static int GetHashCode(TType instance) => VogenDefaults.GetHashCode(instance); diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsUserId.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsUserId.cs index 178bef34..7102e3e0 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsUserId.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsUserId.cs @@ -3,18 +3,14 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = NexusModsUserId; using TValueType = Int32; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct NexusModsUserId : IVogen, IVogenParsable, IVogenSpanParsable, IVogenUtf8SpanParsable, IHasDefaultValue +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct NexusModsUserId : IVogen, IHasDefaultValue { public static readonly TType None = From(0); public static TType DefaultValue => None; public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); public static bool TryParseUrl(string urlRaw, out TType userId) { diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsUserName.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsUserName.cs index cae47a57..f9fbb09b 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsUserName.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/NexusModsUserName.cs @@ -3,18 +3,53 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = NexusModsUserName; using TValueType = String; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None)] -public readonly partial record struct NexusModsUserName : IVogen, IHasDefaultValue +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)] +public readonly partial struct NexusModsUserName : IVogen, IHasDefaultValue { public static readonly TType Empty = From(string.Empty); public static TType DefaultValue => Empty; public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); + + + public static bool TryParseUrl(string urlRaw, out TType username) + { + username = DefaultValue; + + if (!Uri.TryCreate(urlRaw, UriKind.Absolute, out var url)) + return false; + + if (!urlRaw.Contains("nexusmods.com/profile/", StringComparison.OrdinalIgnoreCase)) + return false; + + if (url.LocalPath.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) is not [_, var usernameRaw, ..]) + return false; + + username = From(usernameRaw); + return true; + + /* + username = default; + + if (url is null) + return false; + + if (!url.Contains("nexusmods.com/profile/", StringComparison.OrdinalIgnoreCase)) + return false; + + var str1 = url.ToLowerInvariant().Split("nexusmods.com/profile/"); + if (str1.Length != 2) + return false; + + var split = str1[1].Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length < 2) + return false; + + username = split[2]; + return true; + */ + } public static int GetHashCode(TType instance) => VogenDefaults.GetHashCode(instance); diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/TenantId.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/TenantId.cs index 4a840637..0b792351 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/TenantId.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/TenantId.cs @@ -3,15 +3,15 @@ namespace BUTR.Site.NexusMods.Server.Models; using TType = TenantId; using TValueType = Byte; -[TypeConverter(typeof(VogenTypeConverter))] -[JsonConverter(typeof(VogenJsonConverter))] -[ValueObject(conversions: Conversions.None, deserializationStrictness: DeserializationStrictness.AllowKnownInstances)] -public readonly partial record struct TenantId : IVogen, IVogenParsable, IVogenSpanParsable, IVogenUtf8SpanParsable, IHasDefaultValue +[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter, deserializationStrictness: DeserializationStrictness.AllowKnownInstances)] +public readonly partial struct TenantId : IVogen, IHasDefaultValue { public static readonly TType None = From(0); public static readonly TType Bannerlord = From(TenantUtils.BannerlordId); public static readonly TType Rimworld = From(TenantUtils.RimworldId); public static readonly TType StardewValley = From(TenantUtils.StardewValleyId); + public static readonly TType Valheim = From(TenantUtils.ValheimId); + public static readonly TType Error = From(255); public static TType DefaultValue => None; @@ -22,13 +22,12 @@ public static IEnumerable Values yield return Bannerlord; //yield return Rimworld; //yield return StardewValley; + //yield return Valheim; //yield return DarkestDungeon; } } public static TType Copy(TType instance) => instance with { }; - public static bool IsInitialized(TType instance) => instance._isInitialized; - public static TType DeserializeDangerous(TValueType instance) => Deserialize(instance); public static TType FromTenant(int tenant) { diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Usings.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Usings.cs index 4bbed42f..93b0ccf7 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Usings.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Usings.cs @@ -1,24 +1,21 @@ -global using BUTR.Site.NexusMods.Server.Models; -global using BUTR.Site.NexusMods.Server.Utils; global using BUTR.Site.NexusMods.Server.ValueObjects.Utils; global using BUTR.Site.NexusMods.Shared.Helpers; -global using Microsoft.EntityFrameworkCore; -global using Microsoft.EntityFrameworkCore.ChangeTracking; -global using Microsoft.EntityFrameworkCore.Metadata.Builders; -global using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.OpenApi.Models; - -global using Swashbuckle.AspNetCore.SwaggerGen; - -global using System.ComponentModel; global using System.Diagnostics.CodeAnalysis; -global using System.Globalization; +global using System.Diagnostics.Contracts; global using System.Reflection; global using System.Runtime.CompilerServices; global using System.Runtime.InteropServices; -global using System.Text.Json; -global using System.Text.Json.Serialization; -global using Vogen; \ No newline at end of file +global using Vogen; + +[assembly: VogenDefaults( + isInitializedMethodGeneration: IsInitializedMethodGeneration.Generate, + systemTextJsonConverterFactoryGeneration: SystemTextJsonConverterFactoryGeneration.Generate, + staticAbstractsGeneration: StaticAbstractsGeneration.ExplicitCastFromPrimitive | + StaticAbstractsGeneration.ExplicitCastToPrimitive | + //StaticAbstractsGeneration.ImplicitCastToPrimitive | + StaticAbstractsGeneration.EqualsOperators | + StaticAbstractsGeneration.FactoryMethods | + StaticAbstractsGeneration.InstanceMethodsAndProperties, + openApiSchemaCustomizations: OpenApiSchemaCustomizations.GenerateSwashbuckleSchemaFilter)] \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IHasCopy.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IHasCopy.cs deleted file mode 100644 index 7aa9b222..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IHasCopy.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public interface IHasCopy -{ - static abstract TVogen Copy(TVogen instance); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IHasDeserialize.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IHasDeserialize.cs deleted file mode 100644 index 0c9ab862..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IHasDeserialize.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public interface IHasDeserialize -{ - static abstract TVogen DeserializeDangerous(TValueObject instance); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IHasIsInitialized.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IHasIsInitialized.cs deleted file mode 100644 index 77e9dc48..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IHasIsInitialized.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public interface IHasIsInitialized -{ - static abstract bool IsInitialized(TVogen instance); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IHasRandomValueGenerator.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IHasRandomValueGenerator.cs index c89a3507..4c70b26a 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IHasRandomValueGenerator.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IHasRandomValueGenerator.cs @@ -2,7 +2,6 @@ namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; public interface IHasRandomValueGenerator where TVogen : IVogen - where TValueObject : notnull where TRandom : Random { static abstract TVogen NewRandomValue(TRandom? random); diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IVogen.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IVogen.cs deleted file mode 100644 index 39552a1f..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IVogen.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public interface IVogen : IHasIsInitialized, IHasDeserialize, IHasCopy - where TVogen : IVogen - where TValueObject : notnull -{ - static abstract explicit operator TVogen(TValueObject value); - static abstract explicit operator TValueObject(TVogen value); - - static abstract bool operator ==(TVogen left, TVogen right); - static abstract bool operator !=(TVogen left, TVogen right); - - static abstract bool operator ==(TVogen left, TValueObject right); - static abstract bool operator !=(TVogen left, TValueObject right); - - static abstract bool operator ==(TValueObject left, TVogen right); - static abstract bool operator !=(TValueObject left, TVogen right); - - static abstract TVogen From(TValueObject value); - - static abstract int GetHashCode(TVogen value); - - static abstract bool Equals(TVogen left, TVogen right); - static abstract bool Equals(TVogen left, TVogen right, IEqualityComparer comparer); - - static abstract int CompareTo(TVogen left, TVogen right); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IVogenParsable.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IVogenParsable.cs deleted file mode 100644 index ba0a61b9..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IVogenParsable.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public interface IVogenParsable - where TVogen : IVogen - where TValueObject : IParsable -{ - static abstract bool TryParse(ReadOnlySpan utf8Text, IFormatProvider provider, [NotNullWhen(true)] out TVogen? result); - static abstract bool TryParse(ReadOnlySpan utf8Text, [NotNullWhen(true)] out TVogen? result); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IVogenSpanParsable.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IVogenSpanParsable.cs deleted file mode 100644 index 1424efd2..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IVogenSpanParsable.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public interface IVogenSpanParsable - where TVogen : IVogen - where TValueObject : ISpanParsable -{ - static abstract bool TryParse(ReadOnlySpan s, IFormatProvider provider, [NotNullWhen(true)] out TVogen? result); - static abstract bool TryParse(ReadOnlySpan s, [NotNullWhen(true)] out TVogen? result); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IVogenUtf8SpanParsable.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IVogenUtf8SpanParsable.cs deleted file mode 100644 index 8e1a2b73..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/IVogenUtf8SpanParsable.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public interface IVogenUtf8SpanParsable - where TVogen : IVogen - where TValueObject : IUtf8SpanParsable -{ - static abstract bool TryParse(string s, IFormatProvider provider, [NotNullWhen(true)] out TVogen? result); - static abstract bool TryParse(string s, [NotNullWhen(true)] out TVogen? result); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenDefaults.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenDefaults.cs index cfef668d..4b8194d0 100644 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenDefaults.cs +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenDefaults.cs @@ -2,35 +2,30 @@ namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; public static class VogenDefaults where TVogen : IVogen, IEquatable, IEquatable, IComparable, IComparable - where TValueObject : notnull { - public static TVogen Copy(TVogen value) => TVogen.Copy(value); - - public static TVogen Deserialize(TValueObject value) => TVogen.DeserializeDangerous(value); - public static TValueObject Convert(TVogen value) => (TValueObject) value; public static TVogen Convert(TValueObject value) => (TVogen) value; - public static int GetHashCode(TVogen value) => TVogen.IsInitialized(value) ? value.GetHashCode() : 0; + public static int GetHashCode(TVogen value) => value.IsInitialized() ? value.GetHashCode() : 0; public static bool Equals(TVogen left, TVogen right) { - var leftInitialized = TVogen.IsInitialized(left); - var rightInitialized = TVogen.IsInitialized(right); + var leftInitialized = left.IsInitialized(); + var rightInitialized = right.IsInitialized(); return (!leftInitialized && !rightInitialized) || (leftInitialized && rightInitialized && left.Equals(right)); } public static bool Equals(TVogen left, TVogen right, IEqualityComparer comparer) { - var leftInitialized = TVogen.IsInitialized(left); - var rightInitialized = TVogen.IsInitialized(right); + var leftInitialized = left.IsInitialized(); + var rightInitialized = right.IsInitialized(); return (!leftInitialized && !rightInitialized) || (leftInitialized && rightInitialized && comparer.Equals(left, right)); } public static int CompareTo(TVogen left, TVogen right) { - var leftInitialized = TVogen.IsInitialized(left); - var rightInitialized = TVogen.IsInitialized(right); + var leftInitialized = left.IsInitialized(); + var rightInitialized = right.IsInitialized(); return !leftInitialized && !rightInitialized ? 0 : !leftInitialized ? -1 : !rightInitialized ? 1 : left.CompareTo(right); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenDefaultsParsable.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenDefaultsParsable.cs deleted file mode 100644 index 1e2e5437..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenDefaultsParsable.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public static class VogenDefaultsParsable - where TVogen : IVogen, IEquatable, IEquatable, IComparable - where TValueObject : IParsable -{ - public static bool TryParse(string s, IFormatProvider provider, [NotNullWhen(true)] out TVogen? result) - { - if (TValueObject.TryParse(s, provider, out var r)) - { - result = TVogen.From(r); - return true; - } - - result = default; - return false; - } - - public static bool TryParse(string s, [NotNullWhen(true)] out TVogen? result) - { - if (TValueObject.TryParse(s, null, out var r)) - { - result = TVogen.From(r); - return true; - } - - result = default; - return false; - } -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenDefaultsSpanParsable.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenDefaultsSpanParsable.cs deleted file mode 100644 index fa8b19a8..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenDefaultsSpanParsable.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public static class VogenDefaultsSpanParsable - where TVogen : IVogen, IEquatable, IEquatable, IComparable - where TValueObject : ISpanParsable -{ - public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, [NotNullWhen(true)] out TVogen? result) - { - if (TValueObject.TryParse(s, provider, out var r)) - { - result = TVogen.From(r); - return true; - } - - result = default; - return false; - } - - public static bool TryParse(ReadOnlySpan s, [NotNullWhen(true)] out TVogen? result) - { - if (TValueObject.TryParse(s, null, out var r)) - { - result = TVogen.From(r); - return true; - } - - result = default; - return false; - } -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenDefaultsUtf8SpanParsable.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenDefaultsUtf8SpanParsable.cs deleted file mode 100644 index 00ab4e06..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenDefaultsUtf8SpanParsable.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public static class VogenDefaultsUtf8SpanParsable - where TVogen : IVogen, IEquatable, IEquatable, IComparable - where TValueObject : IUtf8SpanParsable -{ - public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider provider, [NotNullWhen(true)] out TVogen? result) - { - if (TValueObject.TryParse(utf8Text, provider, out var r)) - { - result = TVogen.From(r); - return true; - } - - result = default; - return false; - } - - public static bool TryParse(ReadOnlySpan utf8Text, [NotNullWhen(true)] out TVogen? result) - { - if (TValueObject.TryParse(utf8Text, null, out var r)) - { - result = TVogen.From(r); - return true; - } - - result = default; - return false; - } -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenJsonConverter.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenJsonConverter.cs deleted file mode 100644 index 9940e0a6..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenJsonConverter.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public class VogenJsonConverter : JsonConverter - where TVogen : struct, IVogen, IHasIsInitialized, IEquatable, IEquatable, IComparable - where TValueObject : notnull -{ - public override TVogen Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var primitive = JsonSerializer.Deserialize(ref reader, options)!; - return TVogen.DeserializeDangerous(primitive); - } - - public override void Write(Utf8JsonWriter writer, TVogen value, JsonSerializerOptions options) - { - JsonSerializer.Serialize(writer, (TValueObject) value, options); - } - - public override TVogen ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - //return Read(ref reader, typeToConvert, options); - - var primitive = JsonSerializer.Deserialize(ref reader, options)!; - return TVogen.DeserializeDangerous(primitive); - } - - public override void WriteAsPropertyName(Utf8JsonWriter writer, TVogen value, JsonSerializerOptions options) - { - writer.WritePropertyName(JsonSerializer.Serialize((TValueObject) value, options)); - } -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenSchemaFilter.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenSchemaFilter.cs deleted file mode 100644 index 0bb27e11..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenSchemaFilter.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public sealed class VogenSchemaFilter : ISchemaFilter -{ - public void Apply(OpenApiSchema schema, SchemaFilterContext context) - { - if (context.Type.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IVogen<,>)) is not { } vogen) - return; - - if (vogen.GetGenericArguments() is not [_, { } valueObject]) - return; - - var schemaValueObject = context.SchemaGenerator.GenerateSchema(valueObject, context.SchemaRepository, context.MemberInfo, context.ParameterInfo); - CopyHelper.CopyPublicProperties(schemaValueObject, schema); - } -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenTypeConverter.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenTypeConverter.cs deleted file mode 100644 index 2af884fc..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenTypeConverter.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public class VogenTypeConverter : TypeConverter - where TVogen : struct, IVogen, IHasIsInitialized, IEquatable, IEquatable, IComparable - where TValueObject : notnull -{ - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => - sourceType == typeof(TValueObject) || base.CanConvertFrom(context, sourceType); - - public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) => - value is TValueObject valValue ? TVogen.DeserializeDangerous(valValue) : base.ConvertFrom(context, culture, value); - - public override bool CanConvertTo(ITypeDescriptorContext? context, Type? sourceType) => - sourceType == typeof(TVogen) || base.CanConvertTo(context, sourceType); - - public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) => value is TVogen idValue - ? destinationType == typeof(TValueObject) ? (TValueObject) idValue : base.ConvertTo(context, culture, value, destinationType) - : base.ConvertTo(context, culture, value, destinationType); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenUtils.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenUtils.cs new file mode 100644 index 00000000..c00808e8 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenUtils.cs @@ -0,0 +1,46 @@ +namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; + +public static class VogenUtils +{ + private static readonly Dictionary _delegates = new(); + + public static bool TryGetVogenDefaultValue(Type vogenType, out object? defaultValue) + { + if (_delegates.TryGetValue(vogenType, out defaultValue)) + return true; + + if (vogenType.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IVogen<,>)) is { } vogen) + { + var genericArgs = vogen.GenericTypeArguments; + if (vogenType.IsAssignableTo(typeof(IHasDefaultValue<>))) + { + defaultValue = typeof(VogenUtils).GetMethod("GetDefaultValue", BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(genericArgs[0], genericArgs[1]).Invoke(null, null); + _delegates.TryAdd(vogenType, defaultValue); + return true; + } + defaultValue = typeof(VogenUtils).GetMethod("GetValue", BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(genericArgs[0], genericArgs[1]).Invoke(null, null); + _delegates.TryAdd(vogenType, defaultValue); + return true; + } + + defaultValue = null; + return false; + } + + private static TVogen GetDefaultValue() where TVogen : IVogen, IHasDefaultValue => TVogen.DefaultValue; + private static TVogen GetValue() where TVogen : IVogen => TVogen.From(default!); + + + public static Type ConvertValueObject(Type type) + { + if (type.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IVogen<,>)) is { } vogen) + { + if (vogen.GetGenericArguments() is [_, { } valueObject]) + type = valueObject; + } + + return type; + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenValueComparer.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenValueComparer.cs deleted file mode 100644 index 6ff74880..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenValueComparer.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public sealed class VogenValueComparer : ValueComparer - where TVogen : struct, IVogen, IHasIsInitialized, IEquatable, IEquatable, IComparable, IComparable - where TValueObject : notnull -{ - public VogenValueComparer() : base( - (left, right) => VogenDefaults.Equals(left, right), - instance => VogenDefaults.GetHashCode(instance), - instance => VogenDefaults.Copy(instance)) - { } - - public override int GetHashCode(TVogen instance) => VogenDefaults.GetHashCode(instance); - public override bool Equals(TVogen left, TVogen right) => VogenDefaults.Equals(left, right); - public override TVogen Snapshot(TVogen instance) => VogenDefaults.Copy(instance); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenValueComparerReference.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenValueComparerReference.cs deleted file mode 100644 index 673406a2..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenValueComparerReference.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public sealed class VogenValueComparerReference : ValueComparer - where TVogen : IVogen, IEquatable, IEquatable, IComparable, IComparable - where TValueObject : notnull -{ - public VogenValueComparerReference() : base( - CreateDefaultEqualsExpression(), - instance => VogenDefaults.GetHashCode(instance), - instance => VogenDefaults.Copy(instance)) - { } - - public override int GetHashCode(TVogen instance) => VogenDefaults.GetHashCode(instance); - public override TVogen Snapshot(TVogen instance) => VogenDefaults.Copy(instance); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenValueConverter.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenValueConverter.cs deleted file mode 100644 index 709afb00..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/VogenValueConverter.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.ValueObjects.Utils; - -public sealed class VogenValueConverter : ValueConverter - where TVogen : struct, IVogen, IHasIsInitialized, IEquatable, IEquatable, IComparable, IComparable - where TValueObject : notnull -{ - public VogenValueConverter() : this(null) { } - public VogenValueConverter(ConverterMappingHints? mappingHints = null) : base( - vo => VogenDefaults.Convert(vo), - value => VogenDefaults.Deserialize(value), mappingHints) - { } -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/_CopyHelper.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/_CopyHelper.cs deleted file mode 100644 index 6cd40028..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/_CopyHelper.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.Utils; - -public static class CopyHelper -{ - public static T CopyPublicProperties(T oldObject, T newObject) where T : class - { - const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance; - - if (ReferenceEquals(oldObject, newObject)) return newObject; - - var type = typeof(T); - var propertyList = type.GetProperties(flags); - if (propertyList.Length <= 0) return newObject; - - foreach (var newObjProp in propertyList) - { - var oldProp = type.GetProperty(newObjProp.Name, flags)!; - if (!oldProp.CanRead || !newObjProp.CanWrite) continue; - - var value = oldProp.GetValue(oldObject); - newObjProp.SetValue(newObject, value); - } - - return newObject; - } -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/_QueryableHelper.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/_QueryableHelper.cs deleted file mode 100644 index 1346e9b3..00000000 --- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Utils/_QueryableHelper.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.Utils; - -public static class QueryableHelper -{ - public static Type ConvertValueObject(Type type) - { - if (type.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IVogen<,>)) is { } vogen) - { - if (vogen.GetGenericArguments() is [_, { } valueObject]) - type = valueObject; - } - - return type; - } -} \ No newline at end of file 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 cda62e29..a176a5d8 100644 --- a/src/BUTR.Site.NexusMods.Server/BUTR.Site.NexusMods.Server.csproj +++ b/src/BUTR.Site.NexusMods.Server/BUTR.Site.NexusMods.Server.csproj @@ -17,49 +17,49 @@ - - + + - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - + + + + + + + + - + - + - + - - - + + + - - - - + + + @@ -87,8 +87,8 @@ - - + + \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/BackgroundServices/DiscordLinkedRolesService.cs b/src/BUTR.Site.NexusMods.Server/BackgroundServices/DiscordLinkedRolesService.cs index 1e5b96ea..540121c9 100644 --- a/src/BUTR.Site.NexusMods.Server/BackgroundServices/DiscordLinkedRolesService.cs +++ b/src/BUTR.Site.NexusMods.Server/BackgroundServices/DiscordLinkedRolesService.cs @@ -1,15 +1,15 @@ using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Services; using BUTR.Site.NexusMods.Shared.Helpers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System; using System.Threading; using System.Threading.Tasks; -namespace BUTR.Site.NexusMods.Server.Services; +namespace BUTR.Site.NexusMods.Server.BackgroundServices; [HostedService] public sealed class DiscordLinkedRolesService : BackgroundService @@ -19,8 +19,8 @@ public sealed class DiscordLinkedRolesService : BackgroundService public DiscordLinkedRolesService(ILogger logger, IServiceScopeFactory scopeFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); + _logger = logger; + _scopeFactory = scopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) diff --git a/src/BUTR.Site.NexusMods.Server/BackgroundServices/QuartzListenerBackgroundService.cs b/src/BUTR.Site.NexusMods.Server/BackgroundServices/QuartzListenerBackgroundService.cs index 0be47db5..93d1e7c8 100644 --- a/src/BUTR.Site.NexusMods.Server/BackgroundServices/QuartzListenerBackgroundService.cs +++ b/src/BUTR.Site.NexusMods.Server/BackgroundServices/QuartzListenerBackgroundService.cs @@ -1,10 +1,11 @@ using BUTR.Site.NexusMods.DependencyInjection; -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; +using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.Database; using BUTR.Site.NexusMods.Server.Models.Quartz; +using BUTR.Site.NexusMods.Server.Repositories; +using BUTR.Site.NexusMods.Server.Services; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -14,12 +15,11 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; -namespace BUTR.Site.NexusMods.Server.Services; +namespace BUTR.Site.NexusMods.Server.BackgroundServices; [HostedService] internal sealed class QuartzListenerBackgroundService : BackgroundService @@ -28,16 +28,16 @@ internal sealed class QuartzListenerBackgroundService : BackgroundService private const int MAX_BATCH_SIZE = 50; private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; + private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IQuartzEventProviderService _quartzEventProviderService; - private readonly Channel> _taskQueue; + private readonly Channel> _taskQueue; - public QuartzListenerBackgroundService(ILogger logger, IServiceProvider serviceProvider, IQuartzEventProviderService quartzEventProviderService) + public QuartzListenerBackgroundService(ILogger logger, IServiceScopeFactory serviceScopeFactory, IQuartzEventProviderService quartzEventProviderService) { _logger = logger; - _serviceProvider = serviceProvider; + _serviceScopeFactory = serviceScopeFactory; _quartzEventProviderService = quartzEventProviderService; - _taskQueue = Channel.CreateUnbounded>(new UnboundedChannelOptions { SingleReader = true }); + _taskQueue = Channel.CreateUnbounded>(new UnboundedChannelOptions { SingleReader = true }); _quartzEventProviderService.OnJobToBeExecuted += OnJobToBeExecuted; _quartzEventProviderService.OnJobWasExecuted += OnJobWasExecuted; @@ -56,8 +56,10 @@ public override void Dispose() private void OnJobToBeExecuted(object? sender, QuartzEventArgs e) => QueueInsertTask(CreateScheduleJobLogEntry(e.Args)); - private void OnJobWasExecuted(object? sender, JobWasExecutedEventArgs e) => + private void OnJobWasExecuted(object? sender, JobWasExecutedEventArgs e) + { QueueUpdateTask(CreateScheduleJobLogEntry(e.JobExecutionContext, e.JobException, true)); + } private void OnJobExecutionVetoed(object? sender, QuartzEventArgs e) => QueueUpdateTask(CreateScheduleJobLogEntry(e.Args, defaultIsSuccess: false) with @@ -68,27 +70,23 @@ private void OnJobExecutionVetoed(object? sender, QuartzEventArgs(); + + await MarkIncompleteExecutionAsync(unitOfWorkFactory, ct); while (!ct.IsCancellationRequested) - { - await ProcessTaskAsync(ct); - } + await ProcessTaskAsync(unitOfWorkFactory, ct); } - private async Task MarkIncompleteExecutionAsync(CancellationToken ct) + private async Task MarkIncompleteExecutionAsync(IUnitOfWorkFactory unitOfWorkFactory, CancellationToken ct) { try { - await using var scope = _serviceProvider.CreateAsyncScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - - await dbContext.QuartzExecutionLogs - .Where(x => !x.IsSuccess.HasValue) - .ExecuteUpdateAsync(calls => calls - .SetProperty(x => x.IsSuccess, false) - .SetProperty(x => x.ErrorMessage, "Incomplete execution.") - .SetProperty(x => x.JobRunTime, TimeSpan.Zero), CancellationToken.None); + await using var unitOfWrite = unitOfWorkFactory.CreateUnitOfWrite(TenantId.None); + + await unitOfWrite.QuartzExecutionLogs.MarkIncompleteAsync(ct); + await unitOfWrite.SaveChangesAsync(CancellationToken.None); } catch (OperationCanceledException) { @@ -100,22 +98,18 @@ await dbContext.QuartzExecutionLogs } } - private async Task ProcessTaskAsync(CancellationToken ct = default) + private async Task ProcessTaskAsync(IUnitOfWorkFactory unitOfWorkFactory, CancellationToken ct = default) { var batch = await GetBatchAsync(ct); _logger.LogInformation("Got a batch with {TaskCount} task(s). Saving to data store", batch.Count); - await using var scope = _serviceProvider.CreateAsyncScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); + await using var unitOfWrite = unitOfWorkFactory.CreateUnitOfWrite(TenantId.None); try { - await using var _ = await dbContext.CreateSaveScopeAsync(); - foreach (var workItem in batch) - { - await workItem(dbContext, ct); - } + await workItem(unitOfWrite, ct); + await unitOfWrite.SaveChangesAsync(CancellationToken.None); } catch (OperationCanceledException) { @@ -127,7 +121,7 @@ private async Task ProcessTaskAsync(CancellationToken ct = default) } } - private void QueueTask(Func task) + private void QueueTask(Func task) { if (!_taskQueue.Writer.TryWrite(task)) { @@ -136,11 +130,11 @@ private void QueueTask(Func ta } } - private async Task>> GetBatchAsync(CancellationToken ct) + private async Task>> GetBatchAsync(CancellationToken ct) { await _taskQueue.Reader.WaitToReadAsync(ct); - var batch = new List>(); + var batch = new List>(); while (batch.Count < MAX_BATCH_SIZE && _taskQueue.Reader.TryRead(out var dbTask)) { @@ -155,7 +149,14 @@ private void QueueUpdateTask(QuartzExecutionLogEntity log) => QueueTask(async (d try { await Task.Yield(); - dbContext.QuartzExecutionLogs.Update(log); + var existingJob = await dbContext.QuartzExecutionLogs.FirstOrDefaultAsync(x => + x.RunInstanceId == log.RunInstanceId && + x.JobName == log.JobName && + x.JobGroup == log.JobGroup && + x.TriggerName == log.TriggerName && + x.TriggerGroup == log.TriggerGroup && + x.FireTimeUtc == log.FireTimeUtc, null, ct); + dbContext.QuartzExecutionLogs.Update(existingJob!, log with { LogId = existingJob!.LogId }); } catch (Exception ex) { diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/AppDbContextFactory.cs b/src/BUTR.Site.NexusMods.Server/Contexts/AppDbContextFactory.cs deleted file mode 100644 index 400d3aaf..00000000 --- a/src/BUTR.Site.NexusMods.Server/Contexts/AppDbContextFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -using BUTR.Site.NexusMods.DependencyInjection; - -using Microsoft.EntityFrameworkCore; - -using System.Threading; -using System.Threading.Tasks; - -namespace BUTR.Site.NexusMods.Server.Contexts; - -[ScopedService] -public class AppDbContextFactory : IAppDbContextFactory -{ - private readonly IDbContextFactory _dbContextFactoryWrite; - private readonly IDbContextFactory _dbContextFactoryRead; - - public AppDbContextFactory(IDbContextFactory dbContextFactoryWrite, IDbContextFactory dbContextFactoryRead) - { - _dbContextFactoryWrite = dbContextFactoryWrite; - _dbContextFactoryRead = dbContextFactoryRead; - } - - public IAppDbContextWrite CreateWrite() => _dbContextFactoryWrite.CreateDbContext(); - public async Task CreateWriteAsync(CancellationToken ct) => await _dbContextFactoryWrite.CreateDbContextAsync(ct); - - public IAppDbContextRead CreateRead() => _dbContextFactoryRead.CreateDbContext(); - public async Task CreateReadAsync(CancellationToken ct) => await _dbContextFactoryRead.CreateDbContextAsync(ct); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/AppDbContextProvider.cs b/src/BUTR.Site.NexusMods.Server/Contexts/AppDbContextProvider.cs new file mode 100644 index 00000000..10e90a83 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Contexts/AppDbContextProvider.cs @@ -0,0 +1,15 @@ +using BUTR.Site.NexusMods.DependencyInjection; + +using System; + +namespace BUTR.Site.NexusMods.Server.Contexts; + +[ScopedService] +internal sealed class AppDbContextProvider : IAppDbContextProvider +{ + private BaseAppDbContext _current = default!; + + public void Set(BaseAppDbContext dbContext) => _current = dbContext; + + public BaseAppDbContext Get() => _current ?? throw new InvalidOperationException("AppDbContext has not been set."); +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/AppDbContextRead.cs b/src/BUTR.Site.NexusMods.Server/Contexts/AppDbContextRead.cs index c09830b3..2b1d0daa 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/AppDbContextRead.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/AppDbContextRead.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using System; using System.Threading; @@ -7,7 +6,7 @@ namespace BUTR.Site.NexusMods.Server.Contexts; -public sealed class AppDbContextRead : BaseAppDbContext, IAppDbContextRead +public sealed class AppDbContextRead : BaseAppDbContext { public override bool IsReadOnly => true; @@ -28,8 +27,6 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) base.OnConfiguring(optionsBuilder); } - public AppDbContextRead New() => this.GetService>().CreateDbContext(); - public override int SaveChanges(bool acceptAllChangesOnSuccess) => throw WriteNotSupported(); public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken ct = default) => throw WriteNotSupported(); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/AppDbContextWrite.cs b/src/BUTR.Site.NexusMods.Server/Contexts/AppDbContextWrite.cs index 7b5a4d0d..3821e486 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/AppDbContextWrite.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/AppDbContextWrite.cs @@ -1,60 +1,18 @@ -using BUTR.Site.NexusMods.Server.Extensions; -using BUTR.Site.NexusMods.Server.Models.Database; - using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; namespace BUTR.Site.NexusMods.Server.Contexts; -public sealed class AppDbContextWrite : BaseAppDbContext, IAppDbContextWrite +public sealed class AppDbContextWrite : BaseAppDbContext { - private sealed class AppDbContextSaveScope : IAppDbContextSaveScope - { - public static AppDbContextSaveScope Create(AppDbContextWrite dbContextWrite, Action onDispose) => new(dbContextWrite, onDispose); - - private readonly AppDbContextWrite _dbContextWrite; - private readonly Action _onDispose; - private bool _hasCancelled; - - private AppDbContextSaveScope(AppDbContextWrite dbContextWrite, Action onDispose) - { - _dbContextWrite = dbContextWrite; - _onDispose = onDispose; - } - - public Task CancelAsync() - { - if (!_hasCancelled) _hasCancelled = true; - - return Task.CompletedTask; - } - - public async ValueTask DisposeAsync() - { - try - { - if (!_hasCancelled) - { - await _dbContextWrite.SaveAsync(CancellationToken.None); - } - } - finally - { - _onDispose(); - } - } - } - private readonly ITenantContextAccessor _tenantContextAccessor; + private readonly List> _futureActions = new(); - private EntityFactory? _entityFactory; - private List>? _onSave; + private UpsertEntityFactory? _entityFactory; public AppDbContextWrite( ITenantContextAccessor tenantContextAccessor, @@ -72,89 +30,21 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) base.OnConfiguring(optionsBuilder); } - - public AppDbContextWrite New() => this.GetService>().CreateDbContext(); - - public EntityFactory GetEntityFactory() => _entityFactory ??= new EntityFactory(_tenantContextAccessor, this); - - public Task CreateSaveScopeAsync() - { - _onSave = new(); - return Task.FromResult(AppDbContextSaveScope.Create(this, () => - { - _entityFactory = null; - _onSave = null; - if (ChangeTracker.HasChanges()) - ChangeTracker.Clear(); - })); - } + public UpsertEntityFactory GetEntityFactory() => _entityFactory ??= new UpsertEntityFactory(_tenantContextAccessor, this); public async Task SaveAsync(CancellationToken ct) { - if (!ChangeTracker.HasChanges() && _entityFactory is null && _onSave?.Count == 0) - return; - - var executionStrategy = Database.CreateExecutionStrategy(); - await executionStrategy.ExecuteAsync(this, static async (dbContext, ct) => - { - await using var transaction = await dbContext.Database.BeginTransactionAsync(ct); - try - { - if (dbContext._entityFactory is not null) - await dbContext._entityFactory.SaveCreatedAsync(ct); + if (_entityFactory is not null) + await _entityFactory.SaveCreatedAsync(ct); - foreach (var func in dbContext._onSave ?? Enumerable.Empty>()) - await func(); + foreach (var futureAction in _futureActions) + await futureAction(this); - await dbContext.BulkSaveChangesAsync(o => - { - o.UseInternalTransaction = true; - o.IncludeGraph = false; - o.LegacyIncludeGraph = false; - }, CancellationToken.None); + await SaveChangesAsync(cancellationToken: ct); - await transaction.CommitAsync(ct); - } - catch (Exception e) - { - await transaction.RollbackAsync(ct); - throw; - } - }, ct); + _entityFactory = null; + _futureActions.Clear(); } - public Task BulkUpsertAsync(DbSet dbSet, IEnumerable entities) where TEntity : class, IEntity - { - if (_onSave is null) throw new Exception(); - if (!ReferenceEquals(dbSet.GetService().Context, this)) throw new Exception(); - - _onSave.Add(async () => await dbSet.UpsertAsync(entities)); - return Task.CompletedTask; - } - - public Task BulkSynchronizeAsync(DbSet dbSet, IEnumerable entities) where TEntity : class, IEntity - { - if (_onSave is null) throw new Exception(); - if (!ReferenceEquals(dbSet.GetService().Context, this)) throw new Exception(); - - _onSave.Add(async () => await dbSet.SynchronizeAsync(entities)); - return Task.CompletedTask; - } - - public Task BulkUpsertAsync(DbSet dbSet, IAsyncEnumerable entities) where TEntity : class, IEntity - { - if (_onSave is null) throw new Exception(); - if (!ReferenceEquals(dbSet.GetService().Context, this)) throw new Exception(); - - _onSave.Add(async () => await dbSet.UpsertAsync(entities)); - return Task.CompletedTask; - } - public Task BulkSynchronizeAsync(DbSet dbSet, IAsyncEnumerable entities) where TEntity : class, IEntity - { - if (_onSave is null) throw new Exception(); - if (!ReferenceEquals(dbSet.GetService().Context, this)) throw new Exception(); - - _onSave.Add(async () => await dbSet.SynchronizeAsync(entities)); - return Task.CompletedTask; - } + public void AddFutureAction(Func action) => _futureActions.Add(action); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/BaseAppDbContext.cs b/src/BUTR.Site.NexusMods.Server/Contexts/BaseAppDbContext.cs index c2d650cd..6808eb3b 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/BaseAppDbContext.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/BaseAppDbContext.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; -using System; +using System.Reflection; namespace BUTR.Site.NexusMods.Server.Contexts; @@ -87,6 +87,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.HasPostgresExtension("hstore"); + modelBuilder + .HasDbFunction(typeof(Postgres.Functions).GetRuntimeMethod(nameof(Postgres.Functions.Log), [typeof(decimal)])!) + .HasName("log"); + modelBuilder.HasDbFunction(typeof(Postgres.Functions).GetRuntimeMethod(nameof(Postgres.Functions.Log), [typeof(decimal), typeof(decimal)])!) + .HasName("log"); + _entityConfigurationFactory.ApplyConfigurationWithTenant(modelBuilder); _entityConfigurationFactory.ApplyConfigurationWithTenant(modelBuilder); @@ -145,10 +151,8 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) .ReplaceService() .UseNpgsql(dataSource, opt => { - opt.EnableRetryOnFailure(50, TimeSpan.FromSeconds(5), null); opt.MigrationsHistoryTable("ef_migrations_history", "ef"); }) - //.AddPrepareInterceptor() .EnableSensitiveDataLogging() /*.UseLoggerFactory(LoggerFactory.Create(b => b.AddFilter(level => level >= LogLevel.Information)))*/; } diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/AutocompleteEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/AutocompleteEntityConfiguration.cs index f80fe905..43880efc 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/AutocompleteEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/AutocompleteEntityConfiguration.cs @@ -11,10 +11,14 @@ public AutocompleteEntityConfiguration(ITenantContextAccessor tenantContextAcces protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property("AutocompleteId").HasColumnName("autocomplete_id").ValueGeneratedOnAdd(); + builder.Property(x => x.AutocompleteId).HasColumnName("autocomplete_id").ValueGeneratedOnAdd(); builder.Property(x => x.Type).HasColumnName("type"); builder.Property(x => x.Value).HasColumnName("value"); - builder.ToTable("autocomplete", "autocomplete").HasKey(nameof(AutocompleteEntity.TenantId), "AutocompleteId"); + builder.ToTable("autocomplete", "autocomplete").HasKey(x => new + { + x.TenantId, + x.AutocompleteId, + }); builder.HasIndex(x => x.Type); diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/BaseEntityConfigurationWithTenant.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/BaseEntityConfigurationWithTenant.cs index 59440b4b..3c3458e0 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/BaseEntityConfigurationWithTenant.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/BaseEntityConfigurationWithTenant.cs @@ -4,8 +4,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using System; - namespace BUTR.Site.NexusMods.Server.Contexts.Configs; public abstract class BaseEntityConfigurationWithTenant : BaseEntityConfiguration where TEntity : class, IEntityWithTenant @@ -14,12 +12,12 @@ public abstract class BaseEntityConfigurationWithTenant : BaseEntityCon protected BaseEntityConfigurationWithTenant(ITenantContextAccessor tenantContextAccessor) { - _tenantContextAccessor = tenantContextAccessor ?? throw new ArgumentNullException(nameof(tenantContextAccessor)); + _tenantContextAccessor = tenantContextAccessor; } protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(x => x.TenantId).HasColumnName("tenant").HasValueObjectConversion(); + builder.Property(x => x.TenantId).HasColumnName("tenant").HasVogenConversion(); builder.HasQueryFilter(x => x.TenantId.Equals(_tenantContextAccessor.Current)); builder.HasOne() diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportEntityConfiguration.cs index 96b1eed4..80d1faab 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportEntityConfiguration.cs @@ -12,18 +12,22 @@ public CrashReportEntityConfiguration(ITenantContextAccessor tenantContextAccess protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(x => x.CrashReportId).HasColumnName("crash_report_id").HasValueObjectConversion().ValueGeneratedNever(); - builder.Property(x => x.Version).HasColumnName("version").HasValueObjectConversion(); - builder.Property(x => x.GameVersion).HasColumnName("game_version").HasValueObjectConversion(); - builder.Property(nameof(ExceptionTypeEntity.ExceptionTypeId)).HasColumnName("exception_type_id").HasValueObjectConversion(); + builder.Property(x => x.CrashReportId).HasColumnName("crash_report_id").HasVogenConversion().ValueGeneratedNever(); + builder.Property(x => x.Version).HasColumnName("version").HasVogenConversion(); + builder.Property(x => x.GameVersion).HasColumnName("game_version").HasVogenConversion(); + builder.Property(x => x.ExceptionTypeId).HasColumnName("exception_type_id").HasVogenConversion(); builder.Property(x => x.Exception).HasColumnName("exception"); builder.Property(x => x.CreatedAt).HasColumnName("created_at"); - builder.Property(x => x.Url).HasColumnName("url").HasValueObjectConversion(); - builder.ToTable("crash_report", "crashreport").HasKey(x => new { x.TenantId, x.CrashReportId }); + builder.Property(x => x.Url).HasColumnName("url").HasVogenConversion(); + builder.ToTable("crash_report", "crashreport").HasKey(x => new + { + x.TenantId, + x.CrashReportId + }); builder.HasOne(x => x.ExceptionType) .WithMany(x => x.ToCrashReports) - .HasForeignKey(nameof(CrashReportEntity.TenantId), nameof(ExceptionTypeEntity.ExceptionTypeId)) + .HasForeignKey(x => new { x.TenantId, x.ExceptionTypeId }) .HasPrincipalKey(x => new { x.TenantId, x.ExceptionTypeId }) .OnDelete(DeleteBehavior.Cascade); @@ -32,8 +36,6 @@ protected override void ConfigureModel(EntityTypeBuilder buil //builder.HasIndex(x => x.GameVersion); //builder.HasIndex(x => x.CreatedAt); - builder.Navigation(x => x.ExceptionType).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportIgnoredFileIdEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportIgnoredFileIdEntityConfiguration.cs index f4d3833d..b6478e06 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportIgnoredFileIdEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportIgnoredFileIdEntityConfiguration.cs @@ -1,4 +1,3 @@ -using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.Database; using Microsoft.EntityFrameworkCore; @@ -12,8 +11,8 @@ public CrashReportIgnoredFileIdEntityConfiguration(ITenantContextAccessor tenant protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(x => x.Value).HasColumnName("crash_report_file_ignored_id").HasValueObjectConversion().ValueGeneratedNever(); - builder.ToTable("crash_report_file_ignored", "crashreport").HasKey(x => new { x.TenantId, x.Value }); + builder.Property(x => x.CrashReportFileId).HasColumnName("crash_report_file_ignored_id").ValueGeneratedOnAdd(); + builder.ToTable("crash_report_file_ignored", "crashreport").HasKey(x => new { x.TenantId, Value = x.CrashReportFileId }); base.ConfigureModel(builder); } diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToFileIdEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToFileIdEntityConfiguration.cs index e7a21ea3..fdc92ccd 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToFileIdEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToFileIdEntityConfiguration.cs @@ -12,9 +12,13 @@ public CrashReportToFileIdEntityConfiguration(ITenantContextAccessor tenantConte protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(x => x.CrashReportId).HasColumnName("crash_report_file_id").HasValueObjectConversion().ValueGeneratedNever(); - builder.Property(x => x.FileId).HasColumnName("file_id").HasValueObjectConversion(); - builder.ToTable("crash_report_file", "crashreport").HasKey(x => new { x.TenantId, x.CrashReportId }); + builder.Property(x => x.CrashReportId).HasColumnName("crash_report_file_id").HasVogenConversion().ValueGeneratedNever(); + builder.Property(x => x.FileId).HasColumnName("file_id").HasVogenConversion(); + builder.ToTable("crash_report_file", "crashreport").HasKey(x => new + { + x.TenantId, + x.CrashReportId + }); builder.HasOne(x => x.ToCrashReport) .WithOne(x => x.FileId) diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToMetadataEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToMetadataEntityConfiguration.cs index 7738840f..276b57e4 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToMetadataEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToMetadataEntityConfiguration.cs @@ -12,14 +12,18 @@ public CrashReportToMetadataEntityConfiguration(ITenantContextAccessor tenantCon protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(x => x.CrashReportId).HasColumnName("crash_report_metadata_id").HasValueObjectConversion().ValueGeneratedNever(); + builder.Property(x => x.CrashReportId).HasColumnName("crash_report_metadata_id").HasVogenConversion().ValueGeneratedNever(); builder.Property(x => x.LauncherType).HasColumnName("launcher_type"); builder.Property(x => x.LauncherVersion).HasColumnName("launcher_version"); builder.Property(x => x.Runtime).HasColumnName("runtime"); builder.Property(x => x.BUTRLoaderVersion).HasColumnName("butrloader_version"); builder.Property(x => x.BLSEVersion).HasColumnName("blse_version"); builder.Property(x => x.LauncherExVersion).HasColumnName("launcherex_version"); - builder.ToTable("crash_report_metadata", "crashreport").HasKey(x => new { x.TenantId, x.CrashReportId }); + builder.ToTable("crash_report_metadata", "crashreport").HasKey(x => new + { + x.TenantId, + x.CrashReportId + }); builder.HasOne(x => x.ToCrashReport) .WithOne(x => x.Metadata) diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToModuleMetadataEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToModuleMetadataEntityConfiguration.cs index ad702a29..7f5aab5c 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToModuleMetadataEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToModuleMetadataEntityConfiguration.cs @@ -1,5 +1,6 @@ using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.ValueObjects.Utils; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -12,13 +13,18 @@ public CrashReportToModuleMetadataEntityConfiguration(ITenantContextAccessor ten protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(x => x.CrashReportId).HasColumnName("crash_report_module_info_id").HasValueObjectConversion().ValueGeneratedNever(); - builder.Property(nameof(ModuleEntity.ModuleId)).HasColumnName("module_id").HasValueObjectConversion(); - builder.Property(x => x.Version).HasValueObjectConversion().HasColumnName("version"); - builder.Property(nameof(NexusModsModEntity.NexusModsModId)).HasColumnName("nexusmods_mod_id").HasValueObjectConversion().IsRequired(false); + builder.Property(x => x.CrashReportId).HasColumnName("crash_report_module_info_id").HasVogenConversion().ValueGeneratedNever(); + builder.Property(x => x.ModuleId).HasColumnName("module_id").HasVogenConversion(); + builder.Property(x => x.Version).HasVogenConversion().HasColumnName("version"); + builder.Property(x => x.NexusModsModId).HasColumnName("nexusmods_mod_id").HasConversion().IsRequired(false); builder.Property(x => x.InvolvedPosition).HasColumnName("involved_position"); builder.Property(x => x.IsInvolved).HasColumnName("is_involved"); - builder.ToTable("crash_report_module_info", "crashreport").HasKey(nameof(CrashReportToModuleMetadataEntity.TenantId), nameof(CrashReportToModuleMetadataEntity.CrashReportId), nameof(ModuleEntity.ModuleId)); + builder.ToTable("crash_report_module_info", "crashreport").HasKey(x => new + { + x.TenantId, + x.CrashReportId, + x.ModuleId + }); builder.HasOne(x => x.ToCrashReport) .WithMany(x => x.ModuleInfos) @@ -28,7 +34,7 @@ protected override void ConfigureModel(EntityTypeBuilder x.NexusModsMod) .WithMany() - .HasForeignKey(nameof(NexusModsModEntity.TenantId), nameof(NexusModsModEntity.NexusModsModId)) + .HasForeignKey(x => new { x.TenantId, x.NexusModsModId }) .HasPrincipalKey(x => new { x.TenantId, x.NexusModsModId }) .OnDelete(DeleteBehavior.SetNull); @@ -36,9 +42,6 @@ protected override void ConfigureModel(EntityTypeBuilder x.Module).AutoInclude(); - builder.Navigation(x => x.NexusModsMod).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/ExceptionTypeEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/ExceptionTypeEntityConfiguration.cs index ffc16116..2eeb4e97 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/ExceptionTypeEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/ExceptionTypeEntityConfiguration.cs @@ -12,8 +12,12 @@ public ExceptionTypeEntityConfiguration(ITenantContextAccessor tenantContextAcce protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(x => x.ExceptionTypeId).HasColumnName("exception_type_id").HasValueObjectConversion(); - builder.ToTable("exception_type", "exception").HasKey(x => new { x.TenantId, x.ExceptionTypeId }); + builder.Property(x => x.ExceptionTypeId).HasColumnName("exception_type_id").HasVogenConversion(); + builder.ToTable("exception_type", "exception").HasKey(x => new + { + x.TenantId, + x.ExceptionTypeId + }); base.ConfigureModel(builder); } diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationDiscordTokensEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationDiscordTokensEntityConfiguration.cs index 7f214c84..3f881bd9 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationDiscordTokensEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationDiscordTokensEntityConfiguration.cs @@ -10,16 +10,19 @@ public class IntegrationDiscordTokensEntityConfiguration : BaseEntityConfigurati { protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsUserEntity.NexusModsUserId)).HasColumnName("integration_discord_tokens_id").HasValueObjectConversion().ValueGeneratedNever(); + builder.Property(x => x.NexusModsUserId).HasColumnName("integration_discord_tokens_id").HasVogenConversion().ValueGeneratedNever(); builder.Property(x => x.DiscordUserId).HasColumnName("discord_user_id"); builder.Property(x => x.RefreshToken).HasColumnName("refresh_token"); builder.Property(x => x.AccessToken).HasColumnName("access_token"); builder.Property(x => x.AccessTokenExpiresAt).HasColumnName("access_token_expires_at"); - builder.ToTable("integration_discord_tokens", "integration").HasKey(nameof(NexusModsUserEntity.NexusModsUserId)); + builder.ToTable("integration_discord_tokens", "integration").HasKey(x => new + { + x.NexusModsUserId, + }); builder.HasOne(x => x.NexusModsUser) .WithOne() - .HasForeignKey(nameof(NexusModsUserEntity.NexusModsUserId)) + .HasForeignKey(x => x.NexusModsUserId) .HasPrincipalKey(x => x.NexusModsUserId) .OnDelete(DeleteBehavior.Cascade); @@ -29,8 +32,6 @@ protected override void ConfigureModel(EntityTypeBuilder(x => x.DiscordUserId) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsUser).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationGOGToOwnedTenantEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationGOGToOwnedTenantEntityConfiguration.cs index 370d531d..a6fd59d0 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationGOGToOwnedTenantEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationGOGToOwnedTenantEntityConfiguration.cs @@ -11,8 +11,12 @@ public class IntegrationGOGToOwnedTenantEntityConfiguration : BaseEntityConfigur protected override void ConfigureModel(EntityTypeBuilder builder) { builder.Property(x => x.GOGUserId).HasColumnName("integration_gog_owned_tenant_id"); - builder.Property(x => x.OwnedTenant).HasColumnName("owned_tenant").HasValueObjectConversion(); - builder.ToTable("integration_gog_owned_tenant", "integration").HasKey(x => new { x.GOGUserId, x.OwnedTenant }); + builder.Property(x => x.OwnedTenant).HasColumnName("owned_tenant").HasVogenConversion(); + builder.ToTable("integration_gog_owned_tenant", "integration").HasKey(x => new + { + x.GOGUserId, + x.OwnedTenant + }); builder.HasOne() .WithMany() diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationGOGTokensEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationGOGTokensEntityConfiguration.cs index 4831cc54..0c3d5061 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationGOGTokensEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationGOGTokensEntityConfiguration.cs @@ -10,16 +10,19 @@ public class IntegrationGOGTokensEntityConfiguration : BaseEntityConfiguration builder) { - builder.Property(nameof(NexusModsUserEntity.NexusModsUserId)).HasColumnName("integration_gog_tokens_id").HasValueObjectConversion().ValueGeneratedNever(); + builder.Property(x => x.NexusModsUserId).HasColumnName("integration_gog_tokens_id").HasVogenConversion().ValueGeneratedNever(); builder.Property(x => x.GOGUserId).HasColumnName("gog_user_id"); builder.Property(x => x.RefreshToken).HasColumnName("refresh_token"); builder.Property(x => x.AccessToken).HasColumnName("access_token"); builder.Property(x => x.AccessTokenExpiresAt).HasColumnName("access_token_expires_at"); - builder.ToTable("integration_gog_tokens", "integration").HasKey(nameof(NexusModsUserEntity.NexusModsUserId)); + builder.ToTable("integration_gog_tokens", "integration").HasKey(x => new + { + x.NexusModsUserId, + }); builder.HasOne(x => x.NexusModsUser) .WithOne() - .HasForeignKey(nameof(NexusModsUserEntity.NexusModsUserId)) + .HasForeignKey(x => x.NexusModsUserId) .HasPrincipalKey(x => x.NexusModsUserId) .OnDelete(DeleteBehavior.Cascade); @@ -29,8 +32,6 @@ protected override void ConfigureModel(EntityTypeBuilder(x => x.GOGUserId) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsUser).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationGitHubTokensEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationGitHubTokensEntityConfiguration.cs index 0a2a4b54..4e865f83 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationGitHubTokensEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationGitHubTokensEntityConfiguration.cs @@ -10,14 +10,17 @@ public class IntegrationGitHubTokensEntityConfiguration : BaseEntityConfiguratio { protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsUserEntity.NexusModsUserId)).HasColumnName("integration_github_tokens_id").HasValueObjectConversion().ValueGeneratedNever(); + builder.Property(x => x.NexusModsUserId).HasColumnName("integration_github_tokens_id").HasVogenConversion().ValueGeneratedNever(); builder.Property(x => x.GitHubUserId).HasColumnName("github_user_id"); builder.Property(x => x.AccessToken).HasColumnName("access_token"); - builder.ToTable("integration_github_tokens", "integration").HasKey(nameof(NexusModsUserEntity.NexusModsUserId)); + builder.ToTable("integration_github_tokens", "integration").HasKey(x => new + { + x.NexusModsUserId, + }); builder.HasOne(x => x.NexusModsUser) .WithOne() - .HasForeignKey(nameof(NexusModsUserEntity.NexusModsUserId)) + .HasForeignKey(x => x.NexusModsUserId) .HasPrincipalKey(x => x.NexusModsUserId) .OnDelete(DeleteBehavior.Cascade); @@ -27,8 +30,6 @@ protected override void ConfigureModel(EntityTypeBuilder(x => x.GitHubUserId) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsUser).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationSteamToOwnedTenantEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationSteamToOwnedTenantEntityConfiguration.cs index 3d105e23..2ccac0a6 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationSteamToOwnedTenantEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationSteamToOwnedTenantEntityConfiguration.cs @@ -11,8 +11,12 @@ public class IntegrationSteamToOwnedTenantEntityConfiguration : BaseEntityConfig protected override void ConfigureModel(EntityTypeBuilder builder) { builder.Property(x => x.SteamUserId).HasColumnName("integration_steam_owned_tenant_id"); - builder.Property(x => x.OwnedTenant).HasColumnName("owned_tenant").HasValueObjectConversion(); - builder.ToTable("integration_steam_owned_tenant", "integration").HasKey(x => new { x.SteamUserId, x.OwnedTenant }); + builder.Property(x => x.OwnedTenant).HasColumnName("owned_tenant").HasVogenConversion(); + builder.ToTable("integration_steam_owned_tenant", "integration").HasKey(x => new + { + x.SteamUserId, + x.OwnedTenant + }); builder.HasOne() .WithMany() diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationSteamTokensEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationSteamTokensEntityConfiguration.cs index 33db0816..6141f746 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationSteamTokensEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/IntegrationSteamTokensEntityConfiguration.cs @@ -10,14 +10,17 @@ public sealed class IntegrationSteamTokensEntityConfiguration : BaseEntityConfig { protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsUserEntity.NexusModsUserId)).HasColumnName("integration_steam_tokens_id").HasValueObjectConversion().ValueGeneratedNever(); + builder.Property(x => x.NexusModsUserId).HasColumnName("integration_steam_tokens_id").HasVogenConversion().ValueGeneratedNever(); builder.Property(x => x.SteamUserId).HasColumnName("steam_user_id"); builder.Property(x => x.Data).HasColumnName("data").HasColumnType("hstore"); - builder.ToTable("integration_steam_tokens", "integration").HasKey(nameof(NexusModsUserEntity.NexusModsUserId)); + builder.ToTable("integration_steam_tokens", "integration").HasKey(x => new + { + x.NexusModsUserId, + }); builder.HasOne(x => x.NexusModsUser) .WithOne() - .HasForeignKey(nameof(NexusModsUserEntity.NexusModsUserId)) + .HasForeignKey(x => x.NexusModsUserId) .HasPrincipalKey(x => x.NexusModsUserId) .OnDelete(DeleteBehavior.Cascade); @@ -27,8 +30,6 @@ protected override void ConfigureModel(EntityTypeBuilder(x => x.SteamUserId) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsUser).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/ModuleEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/ModuleEntityConfiguration.cs index 76296185..637e2bc8 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/ModuleEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/ModuleEntityConfiguration.cs @@ -12,8 +12,12 @@ public ModuleEntityConfiguration(ITenantContextAccessor tenantContextAccessor) : protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(x => x.ModuleId).HasColumnName("module_id").HasValueObjectConversion().ValueGeneratedNever(); - builder.ToTable("module", "module").HasKey(x => new { x.TenantId, x.ModuleId }); + builder.Property(x => x.ModuleId).HasColumnName("module_id").HasVogenConversion().ValueGeneratedNever(); + builder.ToTable("module", "module").HasKey(x => new + { + x.TenantId, + x.ModuleId + }); base.ConfigureModel(builder); } diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsArticleEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsArticleEntityConfiguration.cs index cff6b821..45a6286a 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsArticleEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsArticleEntityConfiguration.cs @@ -12,20 +12,22 @@ public NexusModsArticleEntityConfiguration(ITenantContextAccessor tenantContextA protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(x => x.NexusModsArticleId).HasColumnName("nexusmods_article_entity_id").HasValueObjectConversion().ValueGeneratedNever(); + builder.Property(x => x.NexusModsArticleId).HasColumnName("nexusmods_article_entity_id").HasVogenConversion().ValueGeneratedNever(); builder.Property(x => x.Title).HasColumnName("title"); - builder.Property(nameof(NexusModsUserEntity.NexusModsUserId)).HasColumnName("author_id").HasValueObjectConversion(); + builder.Property(x => x.NexusModsUserId).HasColumnName("author_id").HasVogenConversion(); builder.Property(x => x.CreateDate).HasColumnName("create_date"); - builder.ToTable("nexusmods_article_entity", "nexusmods_article").HasKey(x => new { x.TenantId, x.NexusModsArticleId }); + builder.ToTable("nexusmods_article_entity", "nexusmods_article").HasKey(x => new + { + x.TenantId, + x.NexusModsArticleId + }); builder.HasOne(x => x.NexusModsUser) .WithMany(x => x.ToArticles) - .HasForeignKey(nameof(NexusModsUserEntity.NexusModsUserId)) + .HasForeignKey(x => x.NexusModsUserId) .HasPrincipalKey(x => x.NexusModsUserId) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsUser).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModEntityConfiguration.cs index 3f9b1617..8107c9d2 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModEntityConfiguration.cs @@ -12,8 +12,12 @@ public NexusModsModEntityConfiguration(ITenantContextAccessor tenantContextAcces protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(x => x.NexusModsModId).HasColumnName("nexusmods_mod_id").HasValueObjectConversion(); - builder.ToTable("nexusmods_mod", "nexusmods_mod").HasKey(x => new { x.TenantId, x.NexusModsModId }); + builder.Property(x => x.NexusModsModId).HasColumnName("nexusmods_mod_id").HasVogenConversion(); + builder.ToTable("nexusmods_mod", "nexusmods_mod").HasKey(x => new + { + x.TenantId, + x.NexusModsModId + }); base.ConfigureModel(builder); } diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToFileUpdateEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToFileUpdateEntityConfiguration.cs index a08ea157..d19ca9dc 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToFileUpdateEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToFileUpdateEntityConfiguration.cs @@ -12,18 +12,20 @@ public NexusModsModToFileUpdateEntityConfiguration(ITenantContextAccessor tenant protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsModEntity.NexusModsModId)).HasColumnName("nexusmods_mod_file_update_id").HasValueObjectConversion().ValueGeneratedNever(); + builder.Property(x => x.NexusModsModId).HasColumnName("nexusmods_mod_file_update_id").HasVogenConversion().ValueGeneratedNever(); builder.Property(x => x.LastCheckedDate).HasColumnName("date_of_last_check"); - builder.ToTable("nexusmods_mod_file_update", "nexusmods_mod").HasKey(nameof(NexusModsModToFileUpdateEntity.TenantId), nameof(NexusModsModEntity.NexusModsModId)); + builder.ToTable("nexusmods_mod_file_update", "nexusmods_mod").HasKey(x => new + { + x.TenantId, + x.NexusModsModId, + }); builder.HasOne(x => x.NexusModsMod) .WithOne(x => x.FileUpdate) - .HasForeignKey(nameof(NexusModsModToFileUpdateEntity.TenantId), nameof(NexusModsModEntity.NexusModsModId)) + .HasForeignKey(x => new { x.TenantId, x.NexusModsModId }) .HasPrincipalKey(x => new { x.TenantId, x.NexusModsModId }) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsMod).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToModuleEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToModuleEntityConfiguration.cs index 2ac012b3..0bc2ef9e 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToModuleEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToModuleEntityConfiguration.cs @@ -12,29 +12,32 @@ public NexusModsModToModuleEntityConfiguration(ITenantContextAccessor tenantCont protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsModEntity.NexusModsModId)).HasColumnName("nexusmods_mod_module_id").HasValueObjectConversion().ValueGeneratedNever(); - builder.Property(nameof(ModuleEntity.ModuleId)).HasColumnName("module_id").HasValueObjectConversion(); + builder.Property(x => x.NexusModsModId).HasColumnName("nexusmods_mod_module_id").HasVogenConversion().ValueGeneratedNever(); + builder.Property(x => x.ModuleId).HasColumnName("module_id").HasVogenConversion(); builder.Property(x => x.LinkType).HasColumnName("nexusmods_mod_module_link_type_id"); builder.Property(x => x.LastUpdateDate).HasColumnName("date_of_last_update"); - builder.ToTable("nexusmods_mod_module", "nexusmods_mod").HasKey(nameof(NexusModsModToModuleEntity.TenantId), nameof(NexusModsModEntity.NexusModsModId), nameof(ModuleEntity.ModuleId), nameof(NexusModsModToModuleEntity.LinkType)); + builder.ToTable("nexusmods_mod_module", "nexusmods_mod").HasKey(x => new + { + x.TenantId, + x.NexusModsModId, + x.ModuleId, + x.LinkType, + }); builder.HasOne(x => x.NexusModsMod) .WithMany(x => x.ModuleIds) - .HasForeignKey(nameof(NexusModsModToModuleEntity.TenantId), nameof(NexusModsModEntity.NexusModsModId)) + .HasForeignKey(x => new { x.TenantId, x.NexusModsModId }) .HasPrincipalKey(x => new { x.TenantId, x.NexusModsModId }) .OnDelete(DeleteBehavior.Cascade); builder.HasOne(x => x.Module) .WithMany(x => x.ToNexusModsMods) - .HasForeignKey(nameof(NexusModsModToModuleEntity.TenantId), nameof(ModuleEntity.ModuleId)) + .HasForeignKey(x => new { x.TenantId, x.ModuleId }) .HasPrincipalKey(x => new { x.TenantId, x.ModuleId }) .OnDelete(DeleteBehavior.Cascade); //builder.HasIndex(nameof(ModuleEntity.ModuleId)); - 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/Configs/NexusModsModToModuleInfoHistoryEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToModuleInfoHistoryEntityConfiguration.cs index 9b3ca5ce..5a3c1685 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToModuleInfoHistoryEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToModuleInfoHistoryEntityConfiguration.cs @@ -12,35 +12,33 @@ public NexusModsModToModuleInfoHistoryEntityConfiguration(ITenantContextAccessor protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsModEntity.NexusModsModId)).HasColumnName("nexusmods_mod_module_info_history_id").HasValueObjectConversion().ValueGeneratedNever(); - builder.Property(x => x.NexusModsFileId).HasColumnName("nexusmods_file_id").HasValueObjectConversion(); - builder.Property(nameof(ModuleEntity.ModuleId)).HasColumnName("module_id").HasValueObjectConversion(); - builder.Property(x => x.ModuleVersion).HasColumnName("module_version").HasValueObjectConversion(); + builder.Property(x => x.NexusModsModId).HasColumnName("nexusmods_mod_module_info_history_id").HasVogenConversion().ValueGeneratedNever(); + builder.Property(x => x.NexusModsFileId).HasColumnName("nexusmods_file_id").HasVogenConversion(); + builder.Property(x => x.ModuleId).HasColumnName("module_id").HasVogenConversion(); + builder.Property(x => x.ModuleVersion).HasColumnName("module_version").HasVogenConversion(); builder.Property(x => x.ModuleInfo).HasColumnName("module_info").HasColumnType("jsonb"); builder.Property(x => x.UploadDate).HasColumnName("date_of_upload"); - builder.ToTable("nexusmods_mod_module_info_history", "nexusmods_mod").HasKey( - nameof(NexusModsModToModuleInfoHistoryEntity.TenantId), - nameof(NexusModsModToModuleInfoHistoryEntity.NexusModsFileId), - nameof(NexusModsModEntity.NexusModsModId), - nameof(ModuleEntity.ModuleId), - nameof(NexusModsModToModuleInfoHistoryEntity.ModuleVersion) - ); + builder.ToTable("nexusmods_mod_module_info_history", "nexusmods_mod").HasKey(x => new + { + x.TenantId, + x.NexusModsFileId, + x.NexusModsModId, + x.ModuleId, + x.ModuleVersion, + }); builder.HasOne(x => x.NexusModsMod) .WithMany() - .HasForeignKey(nameof(NexusModsModToModuleInfoHistoryEntity.TenantId), nameof(NexusModsModEntity.NexusModsModId)) + .HasForeignKey(x => new { x.TenantId, x.NexusModsModId }) .HasPrincipalKey(x => new { x.TenantId, x.NexusModsModId }) .OnDelete(DeleteBehavior.Cascade); builder.HasOne(x => x.Module) .WithMany() - .HasForeignKey(nameof(NexusModsModToModuleInfoHistoryEntity.TenantId), nameof(ModuleEntity.ModuleId)) + .HasForeignKey(x => new { x.TenantId, x.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/Configs/NexusModsModToModuleInfoHistoryGameVersionEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToModuleInfoHistoryGameVersionEntityConfiguration.cs index 262d8554..2c6595d8 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToModuleInfoHistoryGameVersionEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToModuleInfoHistoryGameVersionEntityConfiguration.cs @@ -21,22 +21,22 @@ protected override void ConfigureModel(EntityTypeBuilder(nameof(NexusModsModEntity.NexusModsModId)).HasColumnName("nexusmods_mod_module_info_history_game_version_id").HasValueObjectConversion().ValueGeneratedNever(); - builder.Property(x => x.NexusModsFileId).HasColumnName("nexusmods_file_id").HasValueObjectConversion(); - builder.Property(nameof(ModuleEntity.ModuleId)).HasColumnName("module_id").HasValueObjectConversion(); - builder.Property(x => x.ModuleVersion).HasColumnName("module_version").HasValueObjectConversion(); - builder.Property(x => x.GameVersion).HasColumnName("game_version").HasValueObjectConversion(); + builder.Property(x => x.NexusModsModId).HasColumnName("nexusmods_mod_module_info_history_game_version_id").HasVogenConversion().ValueGeneratedNever(); + builder.Property(x => x.NexusModsFileId).HasColumnName("nexusmods_file_id").HasVogenConversion(); + builder.Property(x => x.ModuleId).HasColumnName("module_id").HasVogenConversion(); + builder.Property(x => x.ModuleVersion).HasColumnName("module_version").HasVogenConversion(); + builder.Property(x => x.GameVersion).HasColumnName("game_version").HasVogenConversion(); builder.ToTable("nexusmods_mod_module_info_history_game_version", "nexusmods_mod").HasKey(primaryKeys); builder.HasOne(x => x.NexusModsMod) .WithMany() - .HasForeignKey(nameof(NexusModsModToModuleInfoHistoryGameVersionEntity.TenantId), nameof(NexusModsModEntity.NexusModsModId)) + .HasForeignKey(x => new { x.TenantId, x.NexusModsModId }) .HasPrincipalKey(x => new { x.TenantId, x.NexusModsModId }) .OnDelete(DeleteBehavior.Cascade); builder.HasOne(x => x.Module) .WithMany() - .HasForeignKey(nameof(NexusModsModToModuleInfoHistoryGameVersionEntity.TenantId), nameof(ModuleEntity.ModuleId)) + .HasForeignKey(x => new { x.TenantId, x.ModuleId }) .HasPrincipalKey(x => new { x.TenantId, x.ModuleId }) .OnDelete(DeleteBehavior.Cascade); @@ -46,9 +46,6 @@ protected override void ConfigureModel(EntityTypeBuilder 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/Configs/NexusModsModToNameEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToNameEntityConfiguration.cs index 5826ac34..b3a0823a 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToNameEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsModToNameEntityConfiguration.cs @@ -12,18 +12,20 @@ public NexusModsModToNameEntityConfiguration(ITenantContextAccessor tenantContex protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsModEntity.NexusModsModId)).HasColumnName("nexusmods_mod_name_id").HasValueObjectConversion().ValueGeneratedNever(); + builder.Property(x => x.NexusModsModId).HasColumnName("nexusmods_mod_name_id").HasVogenConversion().ValueGeneratedNever(); builder.Property(x => x.Name).HasColumnName("name"); - builder.ToTable("nexusmods_mod_name", "nexusmods_mod").HasKey(nameof(NexusModsModToNameEntity.TenantId), nameof(NexusModsModEntity.NexusModsModId)); + builder.ToTable("nexusmods_mod_name", "nexusmods_mod").HasKey(x => new + { + x.TenantId, + x.NexusModsModId, + }); builder.HasOne(x => x.NexusModsMod) .WithOne(x => x.Name) - .HasForeignKey(nameof(NexusModsModToNameEntity.TenantId), nameof(NexusModsModEntity.NexusModsModId)) + .HasForeignKey(x => new { x.TenantId, x.NexusModsModId }) .HasPrincipalKey(x => new { x.TenantId, x.NexusModsModId }) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsMod).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserEntityConfiguration.cs index 56f35b6f..031d6855 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserEntityConfiguration.cs @@ -10,8 +10,11 @@ public class NexusModsUserEntityConfiguration : BaseEntityConfiguration builder) { - builder.Property(x => x.NexusModsUserId).HasColumnName("nexusmods_user_id").HasValueObjectConversion().ValueGeneratedNever(); - builder.ToTable("nexusmods_user", "nexusmods_user").HasKey(x => x.NexusModsUserId); + builder.Property(x => x.NexusModsUserId).HasColumnName("nexusmods_user_id").HasVogenConversion().ValueGeneratedNever(); + builder.ToTable("nexusmods_user", "nexusmods_user").HasKey(x => new + { + x.NexusModsUserId + }); base.ConfigureModel(builder); } diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToCrashReportEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToCrashReportEntityConfiguration.cs index d583a617..c526ab7d 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToCrashReportEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToCrashReportEntityConfiguration.cs @@ -12,15 +12,20 @@ public NexusModsUserToCrashReportEntityConfiguration(ITenantContextAccessor tena protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsUserEntity.NexusModsUserId)).HasColumnName("nexusmods_user_crash_report_id").HasValueObjectConversion().ValueGeneratedNever(); - builder.Property(x => x.CrashReportId).HasColumnName("crash_report_id").HasValueObjectConversion(); + builder.Property(x => x.NexusModsUserId).HasColumnName("nexusmods_user_crash_report_id").HasVogenConversion().ValueGeneratedNever(); + builder.Property(x => x.CrashReportId).HasColumnName("crash_report_id").HasVogenConversion(); builder.Property(x => x.Status).HasColumnName("status"); builder.Property(x => x.Comment).HasColumnName("comment"); - builder.ToTable("nexusmods_user_crash_report", "nexusmods_user").HasKey(nameof(NexusModsUserToCrashReportEntity.TenantId), nameof(NexusModsUserEntity.NexusModsUserId), nameof(NexusModsUserToCrashReportEntity.CrashReportId)); + builder.ToTable("nexusmods_user_crash_report", "nexusmods_user").HasKey(x => new + { + x.TenantId, + x.NexusModsUserId, + x.CrashReportId, + }); builder.HasOne(x => x.NexusModsUser) .WithMany(x => x.ToCrashReports) - .HasForeignKey(nameof(NexusModsUserEntity.NexusModsUserId)) + .HasForeignKey(x => x.NexusModsUserId) .HasPrincipalKey(x => x.NexusModsUserId) .OnDelete(DeleteBehavior.Cascade); @@ -30,8 +35,6 @@ protected override void ConfigureModel(EntityTypeBuilder new { x.TenantId, x.CrashReportId }) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsUser).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationDiscordEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationDiscordEntityConfiguration.cs index 8f80dab2..ebf6d5aa 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationDiscordEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationDiscordEntityConfiguration.cs @@ -10,18 +10,19 @@ public class NexusModsUserToIntegrationDiscordEntityConfiguration : BaseEntityCo { protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsUserEntity.NexusModsUserId)).HasColumnName("nexusmods_user_to_discord_id").HasValueObjectConversion().ValueGeneratedNever(); + builder.Property(x => x.NexusModsUserId).HasColumnName("nexusmods_user_to_discord_id").HasVogenConversion().ValueGeneratedNever(); builder.Property(x => x.DiscordUserId).HasColumnName("discord_user_id"); - builder.ToTable("nexusmods_user_to_integration_discord", "nexusmods_user").HasKey(nameof(NexusModsUserEntity.NexusModsUserId)); + builder.ToTable("nexusmods_user_to_integration_discord", "nexusmods_user").HasKey(new[] + { + nameof(NexusModsUserEntity.NexusModsUserId) + }); builder.HasOne(x => x.NexusModsUser) .WithOne(x => x.ToDiscord) - .HasForeignKey(nameof(NexusModsUserEntity.NexusModsUserId)) + .HasForeignKey(x => x.NexusModsUserId) .HasPrincipalKey(x => x.NexusModsUserId) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsUser).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationGOGEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationGOGEntityConfiguration.cs index f677d35a..78a5c4e5 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationGOGEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationGOGEntityConfiguration.cs @@ -10,13 +10,16 @@ public class NexusModsUserToIntegrationGOGEntityConfiguration : BaseEntityConfig { protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsUserEntity.NexusModsUserId)).HasColumnName("nexusmods_user_to_gog_id").HasValueObjectConversion().ValueGeneratedNever(); + builder.Property(x => x.NexusModsUserId).HasColumnName("nexusmods_user_to_gog_id").HasVogenConversion().ValueGeneratedNever(); builder.Property(x => x.GOGUserId).HasColumnName("gog_user_id"); - builder.ToTable("nexusmods_user_to_integration_gog", "nexusmods_user").HasKey(nameof(NexusModsUserEntity.NexusModsUserId)); + builder.ToTable("nexusmods_user_to_integration_gog", "nexusmods_user").HasKey(x => new + { + x.NexusModsUserId, + }); builder.HasOne(x => x.NexusModsUser) .WithOne(x => x.ToGOG) - .HasForeignKey(nameof(NexusModsUserEntity.NexusModsUserId)) + .HasForeignKey(x => x.NexusModsUserId) .HasPrincipalKey(x => x.NexusModsUserId) .OnDelete(DeleteBehavior.Cascade); @@ -26,8 +29,6 @@ protected override void ConfigureModel(EntityTypeBuilder x.GOGUserId) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsUser).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationGitHubEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationGitHubEntityConfiguration.cs index 38dd1093..11e0983d 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationGitHubEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationGitHubEntityConfiguration.cs @@ -10,18 +10,19 @@ public class NexusModsUserToIntegrationGitHubEntityConfiguration : BaseEntityCon { protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsUserEntity.NexusModsUserId)).HasColumnName("nexusmods_user_to_github_id").HasValueObjectConversion().ValueGeneratedNever(); + builder.Property(x => x.NexusModsUserId).HasColumnName("nexusmods_user_to_github_id").HasVogenConversion().ValueGeneratedNever(); builder.Property(x => x.GitHubUserId).HasColumnName("github_user_id"); - builder.ToTable("nexusmods_user_to_integration_github", "nexusmods_user").HasKey(nameof(NexusModsUserEntity.NexusModsUserId)); + builder.ToTable("nexusmods_user_to_integration_github", "nexusmods_user").HasKey(x => new + { + x.NexusModsUserId, + }); builder.HasOne(x => x.NexusModsUser) .WithOne(x => x.ToGitHub) - .HasForeignKey(nameof(NexusModsUserEntity.NexusModsUserId)) + .HasForeignKey(x => x.NexusModsUserId) .HasPrincipalKey(x => x.NexusModsUserId) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsUser).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationSteamEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationSteamEntityConfiguration.cs index 6317ee8a..45f7594b 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationSteamEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToIntegrationSteamEntityConfiguration.cs @@ -10,13 +10,16 @@ public class NexusModsUserToIntegrationSteamEntityConfiguration : BaseEntityConf { protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsUserEntity.NexusModsUserId)).HasColumnName("nexusmods_user_to_steam_id").HasValueObjectConversion().ValueGeneratedNever(); + builder.Property(x => x.NexusModsUserId).HasColumnName("nexusmods_user_to_steam_id").HasVogenConversion().ValueGeneratedNever(); builder.Property(x => x.SteamUserId).HasColumnName("steam_user_id"); - builder.ToTable("nexusmods_user_to_integration_steam", "nexusmods_user").HasKey(nameof(NexusModsUserEntity.NexusModsUserId)); + builder.ToTable("nexusmods_user_to_integration_steam", "nexusmods_user").HasKey(x => new + { + x.NexusModsUserId, + }); builder.HasOne(x => x.NexusModsUser) .WithOne(x => x.ToSteam) - .HasForeignKey(nameof(NexusModsUserEntity.NexusModsUserId)) + .HasForeignKey(x => x.NexusModsUserId) .HasPrincipalKey(x => x.NexusModsUserId) .OnDelete(DeleteBehavior.Cascade); @@ -26,8 +29,6 @@ protected override void ConfigureModel(EntityTypeBuilder x.SteamUserId) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsUser).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToModuleEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToModuleEntityConfiguration.cs index 74bceaff..137896aa 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToModuleEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToModuleEntityConfiguration.cs @@ -12,26 +12,29 @@ public NexusModsUserToModuleEntityConfiguration(ITenantContextAccessor tenantCon protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsUserEntity.NexusModsUserId)).HasColumnName("nexusmods_user_module_id").HasValueObjectConversion().ValueGeneratedNever(); - builder.Property(nameof(ModuleEntity.ModuleId)).HasColumnName("module_id").HasValueObjectConversion(); + builder.Property(x => x.NexusModsUserId).HasColumnName("nexusmods_user_module_id").HasVogenConversion().ValueGeneratedNever(); + builder.Property(x => x.ModuleId).HasColumnName("module_id").HasVogenConversion(); builder.Property(x => x.LinkType).HasColumnName("nexusmods_user_module_link_type_id"); - builder.ToTable("nexusmods_user_module", "nexusmods_user").HasKey(nameof(NexusModsUserToModuleEntity.TenantId), nameof(NexusModsUserEntity.NexusModsUserId), nameof(ModuleEntity.ModuleId), nameof(NexusModsUserToModuleEntity.LinkType)); + builder.ToTable("nexusmods_user_module", "nexusmods_user").HasKey(x => new + { + x.TenantId, + x.NexusModsUserId, + x.ModuleId, + x.LinkType, + }); builder.HasOne(x => x.NexusModsUser) .WithMany(x => x.ToModules) - .HasForeignKey(nameof(NexusModsUserEntity.NexusModsUserId)) + .HasForeignKey(x => x.NexusModsUserId) .HasPrincipalKey(x => x.NexusModsUserId) .OnDelete(DeleteBehavior.Cascade); builder.HasOne(x => x.Module) .WithMany(x => x.ToNexusModsUsers) - .HasForeignKey(nameof(NexusModsUserToModuleEntity.TenantId), nameof(ModuleEntity.ModuleId)) + .HasForeignKey(x => new { x.TenantId, x.ModuleId }) .HasPrincipalKey(x => new { x.TenantId, x.ModuleId }) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsUser).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/Configs/NexusModsUserToNameEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToNameEntityConfiguration.cs index 2976f145..c6e4ac96 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToNameEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToNameEntityConfiguration.cs @@ -10,18 +10,19 @@ public class NexusModsUserToNameEntityConfiguration : BaseEntityConfiguration builder) { - builder.Property(nameof(NexusModsUserEntity.NexusModsUserId)).HasColumnName("nexusmods_user_name_id").HasValueObjectConversion().ValueGeneratedNever(); - builder.Property(x => x.Name).HasColumnName("name").HasValueObjectConversion(); - builder.ToTable("nexusmods_user_name", "nexusmods_user").HasKey(nameof(NexusModsUserEntity.NexusModsUserId)); + builder.Property(x => x.NexusModsUserId).HasColumnName("nexusmods_user_name_id").HasVogenConversion().ValueGeneratedNever(); + builder.Property(x => x.Name).HasColumnName("name").HasVogenConversion(); + builder.ToTable("nexusmods_user_name", "nexusmods_user").HasKey(x => new + { + x.NexusModsUserId, + }); builder.HasOne(x => x.NexusModsUser) .WithOne(x => x.Name) - .HasForeignKey(nameof(NexusModsUserEntity.NexusModsUserId)) + .HasForeignKey(x => x.NexusModsUserId) .HasPrincipalKey(x => x.NexusModsUserId) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsUser).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToNexusModsModEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToNexusModsModEntityConfiguration.cs index aa2c8ca3..236eb12f 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToNexusModsModEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToNexusModsModEntityConfiguration.cs @@ -12,26 +12,29 @@ public NexusModsUserToNexusModsModEntityConfiguration(ITenantContextAccessor ten protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsUserEntity.NexusModsUserId)).HasColumnName("nexusmods_user_nexusmods_mod_id").HasValueObjectConversion(); - builder.Property(nameof(NexusModsModEntity.NexusModsModId)).HasColumnName("nexusmods_mod_id").HasValueObjectConversion(); + builder.Property(x => x.NexusModsUserId).HasColumnName("nexusmods_user_nexusmods_mod_id").HasVogenConversion(); + builder.Property(x => x.NexusModsModId).HasColumnName("nexusmods_mod_id").HasVogenConversion(); builder.Property(x => x.LinkType).HasColumnName("nexusmods_user_nexusmods_mod_link_type_id"); - builder.ToTable("nexusmods_user_nexusmods_mod", "nexusmods_user").HasKey(nameof(NexusModsUserToNexusModsModEntity.TenantId), nameof(NexusModsUserEntity.NexusModsUserId), nameof(NexusModsModEntity.NexusModsModId), nameof(NexusModsUserToNexusModsModEntity.LinkType)); + builder.ToTable("nexusmods_user_nexusmods_mod", "nexusmods_user").HasKey(x => new + { + x.TenantId, + x.NexusModsUserId, + x.NexusModsModId, + x.LinkType, + }); builder.HasOne(x => x.NexusModsUser) .WithMany(x => x.ToNexusModsMods) - .HasForeignKey(nameof(NexusModsUserEntity.NexusModsUserId)) + .HasForeignKey(x => x.NexusModsUserId) .HasPrincipalKey(x => x.NexusModsUserId) .OnDelete(DeleteBehavior.Cascade); builder.HasOne(x => x.NexusModsMod) .WithMany(x => x.ToNexusModsUsers) - .HasForeignKey(nameof(NexusModsUserToNexusModsModEntity.TenantId), nameof(NexusModsModEntity.NexusModsModId)) + .HasForeignKey(x => new { x.TenantId, x.NexusModsModId }) .HasPrincipalKey(x => new { x.TenantId, x.NexusModsModId }) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsUser).AutoInclude(); - builder.Navigation(x => x.NexusModsMod).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToRoleEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToRoleEntityConfiguration.cs index 21f3bcb7..061570ce 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToRoleEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/NexusModsUserToRoleEntityConfiguration.cs @@ -12,17 +12,21 @@ public NexusModsUserToRoleEntityConfiguration(ITenantContextAccessor tenantConte protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(NexusModsUserEntity.NexusModsUserId)).HasColumnName("nexusmods_user_role_id").HasValueObjectConversion().ValueGeneratedNever(); - builder.Property(x => x.Role).HasColumnName("role").HasValueObjectConversion(); - builder.Property(x => x.TenantId).HasColumnName("tenant").HasValueObjectConversion(); - builder.ToTable("nexusmods_user_role", "nexusmods_user").HasKey(nameof(NexusModsUserToRoleEntity.TenantId), nameof(NexusModsUserEntity.NexusModsUserId)); + builder.Property(x => x.NexusModsUserId).HasColumnName("nexusmods_user_role_id").HasVogenConversion().ValueGeneratedNever(); + builder.Property(x => x.Role).HasColumnName("role").HasVogenConversion(); + builder.Property(x => x.TenantId).HasColumnName("tenant").HasVogenConversion(); + builder.ToTable("nexusmods_user_role", "nexusmods_user").HasKey(x => new + { + x.TenantId, + x.NexusModsUserId, + }); builder.HasOne(x => x.NexusModsUser) .WithMany(x => x.ToRoles) - .HasForeignKey(nameof(NexusModsUserEntity.NexusModsUserId)) + .HasForeignKey(x => x.NexusModsUserId) .HasPrincipalKey(x => x.NexusModsUserId) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.NexusModsUser).AutoInclude(); + base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/QuartzExecutionLogEntryConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/QuartzExecutionLogEntryConfiguration.cs index 8e6cac49..156b87d5 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/QuartzExecutionLogEntryConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/QuartzExecutionLogEntryConfiguration.cs @@ -43,7 +43,15 @@ protected override void ConfigureModel(EntityTypeBuilder x.DateAddedUtc).HasColumnName("date_added_utc"); builder.Property(x => x.MachineName).HasColumnName("machie_name"); - builder.ToTable("quartz_log", "quartz").HasKey(x => new { x.RunInstanceId, x.JobName, x.JobGroup, x.TriggerName, x.TriggerGroup, x.FireTimeUtc }); + builder.ToTable("quartz_log", "quartz").HasKey(x => new + { + x.RunInstanceId, + x.JobName, + x.JobGroup, + x.TriggerName, + x.TriggerGroup, + x.FireTimeUtc + }); base.ConfigureModel(builder); } diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/StatisticsCrashScoreInvolvedEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/StatisticsCrashScoreInvolvedEntityConfiguration.cs index 862f2d85..0293a8ba 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/StatisticsCrashScoreInvolvedEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/StatisticsCrashScoreInvolvedEntityConfiguration.cs @@ -13,24 +13,26 @@ public StatisticsCrashScoreInvolvedEntityConfiguration(ITenantContextAccessor te protected override void ConfigureModel(EntityTypeBuilder builder) { builder.Property(x => x.StatisticsCrashScoreInvolvedId).HasColumnName("crash_score_involved_id"); - builder.Property(x => x.GameVersion).HasColumnName("game_version").HasValueObjectConversion(); - builder.Property(nameof(ModuleEntity.ModuleId)).HasColumnName("module_id").HasValueObjectConversion(); - builder.Property(x => x.ModuleVersion).HasColumnName("module_version").HasValueObjectConversion(); + builder.Property(x => x.GameVersion).HasColumnName("game_version").HasVogenConversion(); + builder.Property(x => x.ModuleId).HasColumnName("module_id").HasVogenConversion(); + builder.Property(x => x.ModuleVersion).HasColumnName("module_version").HasVogenConversion(); builder.Property(x => x.InvolvedCount).HasColumnName("involved_count"); builder.Property(x => x.NotInvolvedCount).HasColumnName("not_involved_count"); builder.Property(x => x.TotalCount).HasColumnName("total_count"); builder.Property(x => x.RawValue).HasColumnName("value"); builder.Property(x => x.Score).HasColumnName("crash_score"); - builder.ToTable("crash_score_involved", "statistics").HasKey(x => new { x.TenantId, x.StatisticsCrashScoreInvolvedId }); + builder.ToTable("crash_score_involved", "statistics").HasKey(x => new + { + x.TenantId, + x.StatisticsCrashScoreInvolvedId, + }); builder.HasOne(x => x.Module) .WithMany(x => x.ToCrashScore) - .HasForeignKey(nameof(StatisticsCrashScoreInvolvedEntity.TenantId), nameof(ModuleEntity.ModuleId)) + .HasForeignKey(x => new { x.TenantId, x.ModuleId }) .HasPrincipalKey(x => new { x.TenantId, x.ModuleId }) .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.Module).AutoInclude(); - base.ConfigureModel(builder); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/StatisticsTopExceptionsTypeEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/StatisticsTopExceptionsTypeEntityConfiguration.cs index 669fe6d0..2ce5d371 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/StatisticsTopExceptionsTypeEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/StatisticsTopExceptionsTypeEntityConfiguration.cs @@ -12,13 +12,17 @@ public StatisticsTopExceptionsTypeEntityConfiguration(ITenantContextAccessor ten protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(nameof(ExceptionTypeEntity.ExceptionTypeId)).HasColumnName("top_exceptions_type_id").HasValueObjectConversion().ValueGeneratedNever(); + builder.Property(x => x.ExceptionTypeId).HasColumnName("top_exceptions_type_id").HasVogenConversion().ValueGeneratedNever(); builder.Property(x => x.ExceptionCount).HasColumnName("count"); - builder.ToTable("top_exceptions_type", "statistics").HasKey(nameof(StatisticsTopExceptionsTypeEntity.TenantId), nameof(ExceptionTypeEntity.ExceptionTypeId)); + builder.ToTable("top_exceptions_type", "statistics").HasKey(x => new + { + x.TenantId, + x.ExceptionTypeId, + }); builder.HasOne(x => x.ExceptionType) .WithMany(x => x.ToTopExceptionsTypes) - .HasForeignKey(nameof(StatisticsTopExceptionsTypeEntity.TenantId), nameof(ExceptionTypeEntity.ExceptionTypeId)) + .HasForeignKey(x => new { x.TenantId, x.ExceptionTypeId }) .HasPrincipalKey(x => new { x.TenantId, x.ExceptionTypeId }) .OnDelete(DeleteBehavior.Cascade); diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/TenantEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/TenantEntityConfiguration.cs index 06eea4c3..126b2ba8 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/TenantEntityConfiguration.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/TenantEntityConfiguration.cs @@ -10,8 +10,11 @@ public class TenantEntityConfiguration : BaseEntityConfiguration { protected override void ConfigureModel(EntityTypeBuilder builder) { - builder.Property(x => x.TenantId).HasColumnName("tenant_id").HasValueObjectConversion(); - builder.ToTable("tenant", "tenant").HasKey(x => x.TenantId); + builder.Property(x => x.TenantId).HasColumnName("tenant_id").HasVogenConversion(); + builder.ToTable("tenant", "tenant").HasKey(x => new + { + x.TenantId, + }); base.ConfigureModel(builder); } diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/EntityFactory.cs b/src/BUTR.Site.NexusMods.Server/Contexts/EntityFactory.cs deleted file mode 100644 index ccbf9a6d..00000000 --- a/src/BUTR.Site.NexusMods.Server/Contexts/EntityFactory.cs +++ /dev/null @@ -1,215 +0,0 @@ -using BUTR.Site.NexusMods.Server.Extensions; -using BUTR.Site.NexusMods.Server.Models; -using BUTR.Site.NexusMods.Server.Models.Database; - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace BUTR.Site.NexusMods.Server.Contexts; - -public sealed class EntityFactory -{ - private readonly ITenantContextAccessor _tenantContextAccessor; - private readonly IAppDbContextWrite _dbContextWrite; - - private readonly ConcurrentDictionary _nexusModsUsers = new(); - private readonly ConcurrentDictionary _nexusModsUserNames = new(); - private readonly ConcurrentDictionary _nexusModsMods = new(); - private readonly ConcurrentDictionary _modules = new(); - private readonly ConcurrentDictionary _exceptionTypes = new(); - private readonly ConcurrentDictionary _gitHubUsers = new(); - private readonly ConcurrentDictionary _discordUsers = new(); - private readonly ConcurrentDictionary _steamUsers = new(); - private readonly ConcurrentDictionary _gogUsers = new(); - private readonly ConcurrentDictionary _gitHubTokens = new(); - private readonly ConcurrentDictionary _discordTokens = new(); - private readonly ConcurrentDictionary _gogTokens = new(); - private readonly ConcurrentDictionary _steamTokens = new(); - - private readonly SemaphoreSlim _lock = new(1); - - public EntityFactory(ITenantContextAccessor tenantContextAccessor, IAppDbContextWrite dbContextWrite) - { - _tenantContextAccessor = tenantContextAccessor; - _dbContextWrite = dbContextWrite; - } - - public NexusModsUserEntity GetOrCreateNexusModsUser(NexusModsUserId nexusModsUserId) - { - return _nexusModsUsers.GetOrAdd(nexusModsUserId, ValueFactory); - - static NexusModsUserEntity ValueFactory(NexusModsUserId id) => NexusModsUserEntity.Create(id); - } - - public NexusModsUserEntity GetOrCreateNexusModsUserWithName(NexusModsUserId nexusModsUserId, NexusModsUserName nexusModsUserName) - { - var user = _nexusModsUsers.GetOrAdd(nexusModsUserId, ValueFactory, nexusModsUserName); - _ = _nexusModsUserNames.GetOrAdd(nexusModsUserId, ValueFactory2, user); - return user; - - static NexusModsUserEntity ValueFactory(NexusModsUserId id, NexusModsUserName name) => NexusModsUserEntity.CreateWithName(id, name); - static NexusModsUserToNameEntity ValueFactory2(NexusModsUserId id, NexusModsUserEntity user) => user.Name!; - } - - public NexusModsUserToIntegrationSteamEntity GetOrCreateNexusModsUserSteam(NexusModsUserId nexusModsUserId, string steamUserId) - { - return _steamUsers.GetOrAdd(steamUserId, ValueFactory, (this, nexusModsUserId)); - - static NexusModsUserToIntegrationSteamEntity ValueFactory(string steamUserId_, (EntityFactory, NexusModsUserId) tuple) => new() - { - NexusModsUser = tuple.Item1.GetOrCreateNexusModsUser(tuple.Item2), - SteamUserId = steamUserId_, - }; - } - - public NexusModsUserToIntegrationGitHubEntity GetOrCreateNexusModsUserGitHub(NexusModsUserId nexusModsUserId, string gitHubUserId) - { - return _gitHubUsers.GetOrAdd(gitHubUserId, ValueFactory, (this, nexusModsUserId)); - - static NexusModsUserToIntegrationGitHubEntity ValueFactory(string gitHubUserId_, (EntityFactory, NexusModsUserId) tuple) => new() - { - NexusModsUser = tuple.Item1.GetOrCreateNexusModsUser(tuple.Item2), - GitHubUserId = gitHubUserId_, - }; - } - - public NexusModsUserToIntegrationDiscordEntity GetOrCreateNexusModsUserDiscord(NexusModsUserId nexusModsUserId, string discordUserId) - { - return _discordUsers.GetOrAdd(discordUserId, ValueFactory, (this, nexusModsUserId)); - - static NexusModsUserToIntegrationDiscordEntity ValueFactory(string discordUserId_, (EntityFactory, NexusModsUserId) tuple) => new() - { - NexusModsUser = tuple.Item1.GetOrCreateNexusModsUser(tuple.Item2), - DiscordUserId = discordUserId_, - }; - } - - public NexusModsUserToIntegrationGOGEntity GetOrCreateNexusModsUserGOG(NexusModsUserId nexusModsUserId, string gogUserId) - { - return _gogUsers.GetOrAdd(gogUserId, ValueFactory, (this, nexusModsUserId)); - - static NexusModsUserToIntegrationGOGEntity ValueFactory(string gogUserId_, (EntityFactory, NexusModsUserId) tuple) => new() - { - NexusModsUser = tuple.Item1.GetOrCreateNexusModsUser(tuple.Item2), - GOGUserId = gogUserId_, - }; - } - - public IntegrationGitHubTokensEntity GetOrCreateIntegrationGitHubTokens(NexusModsUserId nexusModsUserId, string gitHubUserId, string accessToken) - { - return _gitHubTokens.GetOrAdd(gitHubUserId, ValueFactory, (this, nexusModsUserId, accessToken)); - - static IntegrationGitHubTokensEntity ValueFactory(string gitHubUserId, (EntityFactory, NexusModsUserId, string) tuple) => new() - { - GitHubUserId = gitHubUserId, - NexusModsUser = tuple.Item1.GetOrCreateNexusModsUser(tuple.Item2), - AccessToken = tuple.Item3, - //UserToDiscord = GetOrCreateNexusModsUserDiscord(nexusModsUserId, discordUserId), - }; - } - - public IntegrationDiscordTokensEntity GetOrCreateIntegrationDiscordTokens(NexusModsUserId nexusModsUserId, string discordUserId, string accessToken, string refreshToken, DateTimeOffset accessTokenExpiresAt) - { - return _discordTokens.GetOrAdd(discordUserId, ValueFactory, (this, nexusModsUserId, accessToken, refreshToken, accessTokenExpiresAt)); - - static IntegrationDiscordTokensEntity ValueFactory(string discordUserId, (EntityFactory, NexusModsUserId, string, string, DateTimeOffset) tuple) => new() - { - DiscordUserId = discordUserId, - NexusModsUser = tuple.Item1.GetOrCreateNexusModsUser(tuple.Item2), - AccessToken = tuple.Item3, - RefreshToken = tuple.Item4, - AccessTokenExpiresAt = tuple.Item5, - //UserToDiscord = GetOrCreateNexusModsUserDiscord(nexusModsUserId, discordUserId), - }; - } - - public IntegrationGOGTokensEntity GetOrCreateIntegrationGOGTokens(NexusModsUserId nexusModsUserId, string gogUserId, string accessToken, string refreshToken, DateTimeOffset accessTokenExpiresAt) - { - return _gogTokens.GetOrAdd(gogUserId, ValueFactory, (this, nexusModsUserId, accessToken, refreshToken, accessTokenExpiresAt)); - - static IntegrationGOGTokensEntity ValueFactory(string gogUserId, (EntityFactory, NexusModsUserId, string, string, DateTimeOffset) tuple) => new() - { - GOGUserId = gogUserId, - NexusModsUser = tuple.Item1.GetOrCreateNexusModsUser(tuple.Item2), - AccessToken = tuple.Item3, - RefreshToken = tuple.Item4, - AccessTokenExpiresAt = tuple.Item5, - //UserToGOG = GetOrCreateNexusModsUserGOG(nexusModsUserId, gogUserId), - }; - } - - public IntegrationSteamTokensEntity GetOrCreateIntegrationSteamTokens(NexusModsUserId nexusModsUserId, string steamUserId, Dictionary data) - { - return _steamTokens.GetOrAdd(steamUserId, ValueFactory, (this, nexusModsUserId, data)); - - static IntegrationSteamTokensEntity ValueFactory(string steamUserId, (EntityFactory, NexusModsUserId, Dictionary) tuple) => new() - { - SteamUserId = steamUserId, - NexusModsUser = tuple.Item1.GetOrCreateNexusModsUser(tuple.Item2), - Data = tuple.Item3, - //UserToSteam = GetOrCreateNexusModsUserSteam(nexusModsUserId, steamUserId), - }; - } - - public NexusModsModEntity GetOrCreateNexusModsMod(NexusModsModId nexusModsModId) - { - var tenant = _tenantContextAccessor.Current; - return _nexusModsMods.GetOrAdd(nexusModsModId, ValueFactory, tenant); - - static NexusModsModEntity ValueFactory(NexusModsModId id, TenantId tenant) => NexusModsModEntity.Create(tenant, id); - } - - public ModuleEntity GetOrCreateModule(ModuleId moduleId) - { - var tenant = _tenantContextAccessor.Current; - return _modules.GetOrAdd(moduleId, ValueFactory, tenant); - - static ModuleEntity ValueFactory(ModuleId id, TenantId tenant) => ModuleEntity.Create(tenant, id); - } - - public ExceptionTypeEntity GetOrCreateExceptionType(ExceptionTypeId exception) - { - var tenant = _tenantContextAccessor.Current; - return _exceptionTypes.GetOrAdd(ExceptionTypeEntity.Create(tenant, exception).ExceptionTypeId, ValueFactory, tenant); - - static ExceptionTypeEntity ValueFactory(ExceptionTypeId id, TenantId tenant) => ExceptionTypeEntity.Create(tenant, id); - } - - public async Task SaveCreatedAsync(CancellationToken ct) - { - await _lock.WaitAsync(ct); - - try - { - var hasChange = false; - async Task DoChange(Func action) - { - hasChange = true; - await action(); - } - - if (!_nexusModsUsers.IsEmpty) await DoChange(() => _dbContextWrite.NexusModsUsers.UpsertAsync(_nexusModsUsers.Values)); - if (!_nexusModsUserNames.IsEmpty) await DoChange(() => _dbContextWrite.NexusModsUserToName.UpsertAsync(_nexusModsUserNames.Values)); - if (!_nexusModsMods.IsEmpty) await DoChange(() => _dbContextWrite.NexusModsMods.UpsertAsync(_nexusModsMods.Values)); - if (!_modules.IsEmpty) await DoChange(() => _dbContextWrite.Modules.UpsertAsync(_modules.Values)); - if (!_exceptionTypes.IsEmpty) await DoChange(() => _dbContextWrite.ExceptionTypes.UpsertAsync(_exceptionTypes.Values)); - if (!_gitHubUsers.IsEmpty) await DoChange(() => _dbContextWrite.NexusModsUserToGitHub.UpsertAsync(_gitHubUsers.Values)); - if (!_discordUsers.IsEmpty) await DoChange(() => _dbContextWrite.NexusModsUserToDiscord.UpsertAsync(_discordUsers.Values)); - if (!_steamUsers.IsEmpty) await DoChange(() => _dbContextWrite.NexusModsUserToSteam.UpsertAsync(_steamUsers.Values)); - if (!_gogUsers.IsEmpty) await DoChange(() => _dbContextWrite.NexusModsUserToGOG.UpsertAsync(_gogUsers.Values)); - if (!_gitHubTokens.IsEmpty) await DoChange(() => _dbContextWrite.IntegrationGitHubTokens.UpsertAsync(_gitHubTokens.Values)); - if (!_discordTokens.IsEmpty) await DoChange(() => _dbContextWrite.IntegrationDiscordTokens.UpsertAsync(_discordTokens.Values)); - if (!_gogTokens.IsEmpty) await DoChange(() => _dbContextWrite.IntegrationGOGTokens.UpsertAsync(_gogTokens.Values)); - if (!_steamTokens.IsEmpty) await DoChange(() => _dbContextWrite.IntegrationSteamTokens.UpsertAsync(_steamTokens.Values)); - - return hasChange; - } - finally - { - _lock.Release(); - } - } -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextFactory.cs b/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextFactory.cs deleted file mode 100644 index 12f5b987..00000000 --- a/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextFactory.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace BUTR.Site.NexusMods.Server.Contexts; - -public interface IAppDbContextFactory -{ - IAppDbContextWrite CreateWrite(); - Task CreateWriteAsync(CancellationToken ct = default); - - IAppDbContextRead CreateRead(); - Task CreateReadAsync(CancellationToken ct = default); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextProvider.cs b/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextProvider.cs new file mode 100644 index 00000000..2bf25248 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextProvider.cs @@ -0,0 +1,7 @@ +namespace BUTR.Site.NexusMods.Server.Contexts; + +internal interface IAppDbContextProvider +{ + void Set(BaseAppDbContext dbContext); + BaseAppDbContext Get(); +} \ 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 deleted file mode 100644 index 95a5ca74..00000000 --- a/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextRead.cs +++ /dev/null @@ -1,53 +0,0 @@ -using BUTR.Site.NexusMods.Server.Models.Database; - -using Microsoft.EntityFrameworkCore; - -namespace BUTR.Site.NexusMods.Server.Contexts; - -public interface IAppDbContextRead -{ - DbSet Tenants { get; } - - DbSet Autocompletes { get; } - - DbSet ExceptionTypes { get; } - - DbSet Modules { get; } - - DbSet CrashReports { get; } - DbSet CrashReportToMetadatas { get; } - DbSet CrashReportModuleInfos { get; } - DbSet CrashReportToFileIds { get; } - DbSet CrashReportIgnoredFileIds { get; } - - DbSet NexusModsUsers { get; } - DbSet NexusModsUserToName { get; } - DbSet NexusModsUserToCrashReports { get; } - DbSet NexusModsUserToNexusModsMods { get; } - DbSet NexusModsUserToModules { get; } - - DbSet NexusModsUserToGitHub { get; } - DbSet NexusModsUserToDiscord { get; } - DbSet NexusModsUserToGOG { get; } - DbSet NexusModsUserToSteam { get; } - - DbSet IntegrationGitHubTokens { get; } - DbSet IntegrationDiscordTokens { get; } - DbSet IntegrationGOGTokens { get; } - DbSet IntegrationGOGToOwnedTenants { get; } - DbSet IntegrationSteamTokens { get; } - DbSet IntegrationSteamToOwnedTenants { get; } - - DbSet NexusModsArticles { get; } - - DbSet NexusModsMods { get; } - DbSet NexusModsModName { get; } - DbSet NexusModsModModules { get; } - DbSet NexusModsModToFileUpdates { get; } - DbSet NexusModsModToModuleInfoHistory { get; } - - DbSet StatisticsTopExceptionsTypes { get; } - DbSet StatisticsCrashScoreInvolveds { get; } - - DbSet QuartzExecutionLogs { get; } -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextSaveScope.cs b/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextSaveScope.cs deleted file mode 100644 index 2712d3a1..00000000 --- a/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextSaveScope.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace BUTR.Site.NexusMods.Server.Contexts; - -public interface IAppDbContextSaveScope : IAsyncDisposable -{ - Task CancelAsync(); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextWrite.cs b/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextWrite.cs deleted file mode 100644 index 58e2c79a..00000000 --- a/src/BUTR.Site.NexusMods.Server/Contexts/IAppDbContextWrite.cs +++ /dev/null @@ -1,21 +0,0 @@ -using BUTR.Site.NexusMods.Server.Models.Database; - -using Microsoft.EntityFrameworkCore; - -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace BUTR.Site.NexusMods.Server.Contexts; - -public interface IAppDbContextWrite : IAppDbContextRead -{ - EntityFactory GetEntityFactory(); - - Task CreateSaveScopeAsync(); - - Task BulkUpsertAsync(DbSet dbSet, IEnumerable entities) where TEntity : class, IEntity; - Task BulkSynchronizeAsync(DbSet dbSet, IEnumerable entities) where TEntity : class, IEntity; - - Task BulkUpsertAsync(DbSet dbSet, IAsyncEnumerable entities) where TEntity : class, IEntity; - Task BulkSynchronizeAsync(DbSet dbSet, IAsyncEnumerable entities) where TEntity : class, IEntity; -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Postgres.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Postgres.cs new file mode 100644 index 00000000..18ca5656 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Contexts/Postgres.cs @@ -0,0 +1,12 @@ +using System; + +namespace BUTR.Site.NexusMods.Server.Contexts; + +public static class Postgres +{ + public static class Functions + { + public static decimal Log(decimal d, decimal x) => throw new InvalidOperationException("This method is not meant to be called directly."); + public static decimal Log(decimal x) => throw new InvalidOperationException("This method is not meant to be called directly."); + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/TenantContextAccessor.cs b/src/BUTR.Site.NexusMods.Server/Contexts/TenantContextAccessor.cs index 16ffa391..6694453e 100644 --- a/src/BUTR.Site.NexusMods.Server/Contexts/TenantContextAccessor.cs +++ b/src/BUTR.Site.NexusMods.Server/Contexts/TenantContextAccessor.cs @@ -26,7 +26,7 @@ public TenantId Current if (_httpContextAccessor?.HttpContext?.GetTenant() is { } httpContextTenant) return httpContextTenant; - return TenantId.None; + return TenantId.Error; } set { diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/UpsertEntityFactory.cs b/src/BUTR.Site.NexusMods.Server/Contexts/UpsertEntityFactory.cs new file mode 100644 index 00000000..dbb83dcc --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Contexts/UpsertEntityFactory.cs @@ -0,0 +1,103 @@ +using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Models.Database; + +using EFCore.BulkExtensions; + +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Contexts; + +/// +/// Upsert is a pain in the ass, especially the graph inclusion. +/// Instead, we manually track such entities and save them manually +/// +public sealed class UpsertEntityFactory +{ + private readonly ITenantContextAccessor _tenantContextAccessor; + private readonly AppDbContextWrite _dbContextWrite; + + private readonly ConcurrentDictionary _nexusModsUsers = new(); + private readonly ConcurrentDictionary _nexusModsUserNames = new(); + private readonly ConcurrentDictionary _nexusModsMods = new(); + private readonly ConcurrentDictionary _modules = new(); + private readonly ConcurrentDictionary _exceptionTypes = new(); + + private readonly SemaphoreSlim _lock = new(1); + + public UpsertEntityFactory(ITenantContextAccessor tenantContextAccessor, AppDbContextWrite dbContextWrite) + { + _tenantContextAccessor = tenantContextAccessor; + _dbContextWrite = dbContextWrite; + } + + public NexusModsUserEntity GetOrCreateNexusModsUser(NexusModsUserId nexusModsUserId) + { + return _nexusModsUsers.GetOrAdd(nexusModsUserId, ValueFactory); + + static NexusModsUserEntity ValueFactory(NexusModsUserId id) => NexusModsUserEntity.Create(id); + } + + public NexusModsUserEntity GetOrCreateNexusModsUserWithName(NexusModsUserId nexusModsUserId, NexusModsUserName nexusModsUserName) + { + var user = _nexusModsUsers.GetOrAdd(nexusModsUserId, ValueFactory, nexusModsUserName); + _ = _nexusModsUserNames.GetOrAdd(nexusModsUserId, ValueFactory2, user); + return user; + + static NexusModsUserEntity ValueFactory(NexusModsUserId id, NexusModsUserName name) => NexusModsUserEntity.CreateWithName(id, name); + static NexusModsUserToNameEntity ValueFactory2(NexusModsUserId id, NexusModsUserEntity user) => user.Name!; + } + + public NexusModsModEntity GetOrCreateNexusModsMod(NexusModsModId nexusModsModId) + { + var tenant = _tenantContextAccessor.Current; + return _nexusModsMods.GetOrAdd(nexusModsModId, ValueFactory, tenant); + + static NexusModsModEntity ValueFactory(NexusModsModId id, TenantId tenant) => NexusModsModEntity.Create(tenant, id); + } + + public ModuleEntity GetOrCreateModule(ModuleId moduleId) + { + var tenant = _tenantContextAccessor.Current; + return _modules.GetOrAdd(moduleId, ValueFactory, tenant); + + static ModuleEntity ValueFactory(ModuleId id, TenantId tenant) => ModuleEntity.Create(tenant, id); + } + + public ExceptionTypeEntity GetOrCreateExceptionType(ExceptionTypeId exception) + { + var tenant = _tenantContextAccessor.Current; + return _exceptionTypes.GetOrAdd(ExceptionTypeEntity.Create(tenant, exception).ExceptionTypeId, ValueFactory, tenant); + + static ExceptionTypeEntity ValueFactory(ExceptionTypeId id, TenantId tenant) => ExceptionTypeEntity.Create(tenant, id); + } + + public async Task SaveCreatedAsync(CancellationToken ct) + { + await _lock.WaitAsync(ct); + + try + { + var hasChange = false; + async Task DoChangeAsync(Func action) + { + hasChange = true; + await action(); + } + + if (!_nexusModsUsers.IsEmpty) await DoChangeAsync(() => _dbContextWrite.BulkInsertOrUpdateAsync(_nexusModsUsers.Values, o => o.IncludeGraph = false, cancellationToken: ct)); + if (!_nexusModsUserNames.IsEmpty) await DoChangeAsync(() => _dbContextWrite.BulkInsertOrUpdateAsync(_nexusModsUserNames.Values, o => o.IncludeGraph = false, cancellationToken: ct)); + if (!_nexusModsMods.IsEmpty) await DoChangeAsync(() => _dbContextWrite.BulkInsertOrUpdateAsync(_nexusModsMods.Values, o => o.IncludeGraph = false, cancellationToken: ct)); + if (!_modules.IsEmpty) await DoChangeAsync(() => _dbContextWrite.BulkInsertOrUpdateAsync(_modules.Values, o => o.IncludeGraph = false, cancellationToken: ct)); + if (!_exceptionTypes.IsEmpty) await DoChangeAsync(() => _dbContextWrite.BulkInsertOrUpdateAsync(_exceptionTypes.Values, o => o.IncludeGraph = false, cancellationToken: ct)); + + return hasChange; + } + finally + { + _lock.Release(); + } + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/AuthenticationController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/AuthenticationController.cs index a9451106..ef8cd7fe 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/AuthenticationController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/AuthenticationController.cs @@ -1,9 +1,9 @@ using BUTR.Authentication.NexusMods.Authentication; using BUTR.Authentication.NexusMods.Services; -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.API; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Services; using BUTR.Site.NexusMods.Server.Utils; using BUTR.Site.NexusMods.Server.Utils.BindingSources; @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -30,44 +30,43 @@ namespace BUTR.Site.NexusMods.Server.Controllers; [ApiController, Route("api/v1/[controller]"), ButrNexusModsAuthorization] public sealed class AuthenticationController : ApiControllerBase { + public sealed record NexusModsOAuthUrlModel(string Url, Guid State); + + private readonly ILogger _logger; private readonly INexusModsAPIClient _nexusModsAPIClient; - private readonly IAppDbContextRead _dbContextRead; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; private readonly ITokenGenerator _tokenGenerator; private readonly JsonSerializerOptions _jsonSerializerOptions; - + private readonly INexusModsUsersClient _nexusModsUsersClient; + private readonly IMemoryCache _memoryCache; public AuthenticationController( ILogger logger, INexusModsAPIClient nexusModsAPIClient, - IAppDbContextRead dbContextRead, + IUnitOfWorkFactory unitOfWorkFactory, ITokenGenerator tokenGenerator, - IOptions jsonSerializerOptions) + IOptions jsonSerializerOptions, INexusModsUsersClient nexusModsUsersClient, IMemoryCache memoryCache) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _nexusModsAPIClient = nexusModsAPIClient ?? throw new ArgumentNullException(nameof(nexusModsAPIClient)); - _dbContextRead = dbContextRead ?? throw new ArgumentNullException(nameof(dbContextRead)); - _tokenGenerator = tokenGenerator ?? throw new ArgumentNullException(nameof(tokenGenerator)); - _jsonSerializerOptions = jsonSerializerOptions.Value ?? throw new ArgumentNullException(nameof(jsonSerializerOptions)); + _logger = logger; + _nexusModsAPIClient = nexusModsAPIClient; + _unitOfWorkFactory = unitOfWorkFactory; + _tokenGenerator = tokenGenerator; + _nexusModsUsersClient = nexusModsUsersClient; + _memoryCache = memoryCache; + _jsonSerializerOptions = jsonSerializerOptions.Value; } - [HttpGet("Authenticate"), AllowAnonymous] + [HttpPost("Authenticate"), AllowAnonymous] [ProducesResponseType(typeof(ApiResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResult), StatusCodes.Status401Unauthorized)] - public async Task> AuthenticateAsync([Required, FromHeader] NexusModsApiKey apiKey, [BindTenant] TenantId tenant, CancellationToken ct) + public async Task> AuthenticateWithApiKeyAsync([FromHeader, Required] NexusModsApiKey apiKey, [BindTenant] TenantId tenant, CancellationToken ct) { if (await _nexusModsAPIClient.ValidateAPIKeyAsync(apiKey, ct) is not { } validateResponse) return ApiResultError("Invalid apiKey!", StatusCodes.Status401Unauthorized); - var userEntity = await _dbContextRead.NexusModsUsers - .Include(x => x.ToRoles) - .Include(x => x.ToGitHub!).ThenInclude(x => x.ToTokens) - .Include(x => x.ToDiscord!).ThenInclude(x => x.ToTokens) - .Include(x => x.ToGOG!).ThenInclude(x => x.ToTokens) - .Include(x => x.ToGOG!).ThenInclude(x => x.ToOwnedTenants) - .Include(x => x.ToSteam!).ThenInclude(x => x.ToTokens) - .Include(x => x.ToSteam!).ThenInclude(x => x.ToOwnedTenants) - .AsSplitQuery() - .FirstOrDefaultAsync(x => x.NexusModsUserId == validateResponse.UserId, ct); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); + + var userEntity = await unitOfRead.NexusModsUsers.GetUserWithIntegrationsAsync(validateResponse.UserId, ct); var role = userEntity?.ToRoles.FirstOrDefault(x => x.TenantId == tenant)?.Role ?? ApplicationRole.User; var typedMetadata = UserTypedMetadata.FromUser(userEntity); @@ -84,31 +83,36 @@ public AuthenticationController( IsSupporter = validateResponse.IsSupporter, IsPremium = validateResponse.IsPremium, APIKey = validateResponse.Key.Value, + AccessToken = null, + RefreshToken = null, Role = role.Value, Metadata = metadata }); return ApiResult(new JwtTokenResponse(generatedToken.Token, HttpContext.GetProfile(validateResponse, role, metadata))); } - [HttpGet("Validate"), AllowAnonymous] + [HttpPost("Validate"), AllowAnonymous] [ProducesResponseType(typeof(ApiResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResult), StatusCodes.Status401Unauthorized)] - public async Task> ValidateAsync([BindApiKey] NexusModsApiKey apiKey, [BindRole] ApplicationRole role, [BindTenant] TenantId tenant, CancellationToken ct) + public async Task> ValidateAsync([BindRole] ApplicationRole role, [BindTenant] TenantId tenant, CancellationToken ct) + { + if (HttpContext.GetAPIKey() is { } apiKey && apiKey != NexusModsApiKey.None) + return await ValidateAPIKeyAsync(apiKey, role, tenant, ct); + + if (HttpContext.GetTokens() is { } tokens) + return await ValidateTokenAsync(tokens, role, tenant, ct); + + return ApiResultError("", StatusCodes.Status401Unauthorized); + } + + private async Task> ValidateAPIKeyAsync(NexusModsApiKey apiKey, ApplicationRole role, TenantId tenant, CancellationToken ct) { if (await _nexusModsAPIClient.ValidateAPIKeyAsync(apiKey, ct) is not { } validateResponse) return ApiResultError("API Key not valid", StatusCodes.Status401Unauthorized); - var userEntity = await _dbContextRead.NexusModsUsers - .Include(x => x.ToRoles) - .Include(x => x.ToGitHub!).ThenInclude(x => x.ToTokens) - .Include(x => x.ToDiscord!).ThenInclude(x => x.ToTokens) - .Include(x => x.ToGOG!).ThenInclude(x => x.ToTokens) - .Include(x => x.ToGOG!).ThenInclude(x => x.ToOwnedTenants) - .Include(x => x.ToSteam!).ThenInclude(x => x.ToTokens) - .Include(x => x.ToSteam!).ThenInclude(x => x.ToOwnedTenants) - .AsSplitQuery() - .FirstOrDefaultAsync(x => x.NexusModsUserId == validateResponse.UserId, ct); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); + var userEntity = await unitOfRead.NexusModsUsers.GetUserWithIntegrationsAsync(validateResponse.UserId, ct); var userRole = userEntity?.ToRoles.FirstOrDefault(x => x.TenantId == tenant)?.Role ?? ApplicationRole.User; var typedMetadata = UserTypedMetadata.FromUser(userEntity); @@ -131,8 +135,10 @@ public AuthenticationController( IsSupporter = validateResponse.IsSupporter, IsPremium = validateResponse.IsPremium, APIKey = validateResponse.Key.Value, + AccessToken = null, + RefreshToken = null, Role = userRole.Value, - Metadata = metadata + Metadata = metadata, }); return ApiResult(new JwtTokenResponse(generatedToken.Token, HttpContext.GetProfile(validateResponse, userRole, metadata))); } @@ -141,4 +147,104 @@ public AuthenticationController( var token = authenticationHeaderValue.Parameter!; return ApiResult(new JwtTokenResponse(token, HttpContext.GetProfile())); } + + [AllowAnonymous] + [HttpGet("OAuthUrl")] + public ApiResult GetOAuthUrl() + { + var (url, codeVerifier, state) = _nexusModsUsersClient.GetOAuthUrl(); + _memoryCache.Set($"NMOAUTH-{state}", codeVerifier, TimeSpan.FromMinutes(10)); + return ApiResult(new NexusModsOAuthUrlModel(url, state)); + } + + [AllowAnonymous] + [HttpGet("AuthenticateCallback")] + public async Task> AuthenticateWithOAuth2Async([FromQuery] string code, [FromQuery] Guid state, [BindTenant] TenantId tenant, CancellationToken ct) + { + var codeVerifier = _memoryCache.Get($"NMOAUTH-{state}"); + if (string.IsNullOrWhiteSpace(codeVerifier)) + return ApiBadRequest("Invalid state"); + + if (await _nexusModsUsersClient.CreateTokensAsync(code, codeVerifier, ct) is not { } tokens) + return ApiResultError("Invalid code!", StatusCodes.Status401Unauthorized); + + if (await _nexusModsUsersClient.GetUserInfoAsync(tokens, ct) is not { } userInfo) + return ApiResultError("Failed to get User Info!", StatusCodes.Status401Unauthorized); + + if (!NexusModsUserId.TryParse(userInfo.UserId, out var nexusModsUserId)) + return ApiResultError("Unvalid User Id!", StatusCodes.Status401Unauthorized); + + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); + + var userEntity = await unitOfRead.NexusModsUsers.GetUserWithIntegrationsAsync(nexusModsUserId, ct); + var role = userEntity?.ToRoles.FirstOrDefault(x => x.TenantId == tenant)?.Role ?? ApplicationRole.User; + + var typedMetadata = UserTypedMetadata.FromUser(userEntity); + var metadata = new Dictionary + { + { nameof(UserTypedMetadata), JsonSerializer.Serialize(typedMetadata, _jsonSerializerOptions) } + }; + var generatedToken = await _tokenGenerator.GenerateTokenAsync(new ButrNexusModsUserInfo + { + UserId = (uint) nexusModsUserId.Value, + Name = userInfo.Name.Value, + EMail = userInfo.Email.Value, + ProfileUrl = userInfo.AvatarUrl, + IsSupporter = userInfo.MembershipRoles.Contains("supporter"), + IsPremium = userInfo.MembershipRoles.Contains("premium"), + APIKey = null, + AccessToken = tokens.AccessToken, + RefreshToken = tokens.RefreshToken, + Role = role.Value, + Metadata = metadata + }); + return ApiResult(new JwtTokenResponse(generatedToken.Token, HttpContext.GetProfile(userInfo, role, metadata))); + } + + private async Task> ValidateTokenAsync(NexusModsOAuthTokens tokens, ApplicationRole role, TenantId tenant, CancellationToken ct) + { + if (await _nexusModsUsersClient.GetUserInfoAsync(tokens, ct) is not { } userInfo) + return ApiResultError("Failed to get User Info!", StatusCodes.Status401Unauthorized); + + if (!NexusModsUserId.TryParse(userInfo.UserId, out var nexusModsUserId)) + return ApiResultError("Unvalid User Id!", StatusCodes.Status401Unauthorized); + + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); + + var userEntity = await unitOfRead.NexusModsUsers.GetUserWithIntegrationsAsync(nexusModsUserId, ct); + var userRole = userEntity?.ToRoles.FirstOrDefault(x => x.TenantId == tenant)?.Role ?? ApplicationRole.User; + + var typedMetadata = UserTypedMetadata.FromUser(userEntity); + var metadata = new Dictionary + { + { nameof(UserTypedMetadata), JsonSerializer.Serialize(typedMetadata, _jsonSerializerOptions) } + }; + + var existingMetadata = HttpContext.GetMetadata(); + var isMetadataEqual = metadata.Count == existingMetadata.Count && metadata.All( + d1KV => existingMetadata.TryGetValue(d1KV.Key, out var d2Value) && (d1KV.Value == d2Value || d1KV.Value.Equals(d2Value))); + + if (userRole != role || !isMetadataEqual) + { + var generatedToken = await _tokenGenerator.GenerateTokenAsync(new ButrNexusModsUserInfo + { + UserId = (uint) nexusModsUserId.Value, + Name = userInfo.Name.Value, + EMail = userInfo.Email.Value, + ProfileUrl = $"https://www.nexusmods.com/users/{userInfo.UserId}", + IsSupporter = userInfo.MembershipRoles.Contains("supporter"), + IsPremium = userInfo.MembershipRoles.Contains("premium"), + APIKey = null, + AccessToken = tokens.AccessToken, + RefreshToken = tokens.RefreshToken, + Role = role.Value, + Metadata = metadata, + }); + return ApiResult(new JwtTokenResponse(generatedToken.Token, HttpContext.GetProfile(userInfo, userRole, metadata))); + } + + var authenticationHeaderValue = AuthenticationHeaderValue.Parse(Request.Headers.Authorization!); + var token = authenticationHeaderValue.Parameter!; + return ApiResult(new JwtTokenResponse(token, HttpContext.GetProfile())); + } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/CrashReportsAnalyzerController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/CrashReportsAnalyzerController.cs index b97c3412..7a25ef4f 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/CrashReportsAnalyzerController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/CrashReportsAnalyzerController.cs @@ -5,15 +5,15 @@ using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Utils; using BUTR.Site.NexusMods.Server.Utils.BindingSources; -using BUTR.Site.NexusMods.Server.Utils.Http.ApiResults; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; @@ -33,16 +33,16 @@ public sealed record CrashReportDiagnosticsResult private readonly ILogger _logger; - private readonly IAppDbContextRead _dbContextRead; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; - public CrashReportsAnalyzerController(ILogger logger, IAppDbContextRead dbContextRead) + public CrashReportsAnalyzerController(ILogger logger, IUnitOfWorkFactory unitOfWorkFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _dbContextRead = dbContextRead ?? throw new ArgumentNullException(nameof(dbContextRead)); + _logger = logger; + _unitOfWorkFactory = unitOfWorkFactory; } - [HttpPost("GetDiagnostics")] - public async Task> GetDiagnosticsAsync([BindTenant] TenantId tenant, [FromBody] CrashReportModel crashReport, [FromServices] ITenantContextAccessor tenantContextAccessor, CancellationToken ct) + [HttpPost("Diagnostics")] + public async Task> GetDiagnosticsAsync([FromBody, Required] CrashReportModel crashReport, [BindTenant] TenantId tenant, [FromServices] ITenantContextAccessor tenantContextAccessor, CancellationToken ct) { tenantContextAccessor.Current = tenant; @@ -111,7 +111,7 @@ private async IAsyncEnumerable GetModuleUpdatesForBannerlordAsync( var currentModuleIdsWithoutAnyData = crashReport.Modules.Where(x => !x.IsOfficial && string.IsNullOrEmpty(x.Url) && x.UpdateInfo is null) .Select(x => ModuleId.From(x.Id)).ToArray(); - var currentMexusModsUpdateInfos = crashReport.Modules.Where(x => !x.IsOfficial && x.UpdateInfo is { Provider: "NexusMods" }) + var currentMexusModsUpdateInfos = crashReport.Modules.Where(x => x is { IsOfficial: false, UpdateInfo.Provider: "NexusMods" }) .Select(x => NexusModsModId.TryParse(x.UpdateInfo!.Value, out var modId) ? modId : NexusModsModId.None) .Where(x => x != NexusModsModId.None).ToArray(); var currentNexusModsIds = crashReport.Modules.Where(x => !x.IsOfficial && !string.IsNullOrEmpty(x.Url)) @@ -122,16 +122,18 @@ private async IAsyncEnumerable GetModuleUpdatesForBannerlordAsync( var currentModules = crashReport.Modules .Where(x => !x.IsOfficial).Select(x => new { ModuleId = ModuleId.From(x.Id), Version = ModuleVersion.From(x.Version) }).ToArray(); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); + // SMAPI uses different update providers - Chucklefish, NexusMods, GitHub // We curectly will only use NexusMods //var updateInfoEntries = _dbContextRead.NexusModsModToModuleInfoHistory.Where(x => moduleIds.Contains(x.Module.ModuleId)); //var entries = _dbContextRead.NexusModsModToModuleInfoHistory.Where(x => moduleIds.Contains(x.Module.ModuleId)); - var historicEntriesBasedOnModuleId = _dbContextRead.NexusModsModToModuleInfoHistory.Where(x => currentModuleIdsWithoutAnyData.Contains(x.Module.ModuleId)); - var historicEntriesBasedOnNexusModsId = _dbContextRead.NexusModsModToModuleInfoHistory.Where(x => currentNexusModsIds.Contains(x.NexusModsMod.NexusModsModId)); - var historicEntriesBasedOnUpdateInfo = _dbContextRead.NexusModsModToModuleInfoHistory.Where(x => currentMexusModsUpdateInfos.Contains(x.NexusModsMod.NexusModsModId)); + var historicEntriesBasedOnModuleId = await unitOfRead.NexusModsModToModuleInfoHistory.GetAllAsync(x => currentModuleIdsWithoutAnyData.Contains(x.Module.ModuleId), null, ct); + var historicEntriesBasedOnNexusModsId = await unitOfRead.NexusModsModToModuleInfoHistory.GetAllAsync(x => currentNexusModsIds.Contains(x.NexusModsMod.NexusModsModId), null, ct); + var historicEntriesBasedOnUpdateInfo = await unitOfRead.NexusModsModToModuleInfoHistory.GetAllAsync(x => currentMexusModsUpdateInfos.Contains(x.NexusModsMod.NexusModsModId), null, ct); var allHistoricEntries = historicEntriesBasedOnModuleId.Concat(historicEntriesBasedOnNexusModsId).Concat(historicEntriesBasedOnUpdateInfo); - var historicEntriesCompatibleWithGameVersion = allHistoricEntries.AsEnumerable().Select(x => new + var historicEntriesCompatibleWithGameVersion = allHistoricEntries.Select(x => new { ModuleId = x.Module.ModuleId, ModuleVersion = ApplicationVersion.TryParse(x.ModuleVersion.Value, out var v) ? v : ApplicationVersion.Empty, diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/CrashReportsController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/CrashReportsController.cs index 36ff4a99..d1dcda81 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/CrashReportsController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/CrashReportsController.cs @@ -1,23 +1,20 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.API; using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Utils; using BUTR.Site.NexusMods.Server.Utils.BindingSources; using BUTR.Site.NexusMods.Server.Utils.Http.ApiResults; using BUTR.Site.NexusMods.Server.Utils.Http.StreamingMultipartResults; -using BUTR.Site.NexusMods.Shared.Helpers; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; @@ -36,102 +33,65 @@ public sealed record CrashReportModel2 public required string Exception { get; init; } public required DateTimeOffset Date { get; init; } public required CrashReportUrl Url { get; init; } - public required ImmutableArray InvolvedModules { get; init; } + public required ModuleId[] InvolvedModules { get; init; } public CrashReportStatus Status { get; init; } = CrashReportStatus.New; public string Comment { get; init; } = string.Empty; } - private sealed record ModuleIdToVersionView + + private readonly ILogger _logger; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; + + public CrashReportsController(ILogger logger, IUnitOfWorkFactory unitOfWorkFactory) { - public required ModuleId ModuleId { get; init; } - public required ModuleVersion Version { get; init; } + _logger = logger; + _unitOfWorkFactory = unitOfWorkFactory; } - private sealed record UserCrashReportView - { - public required CrashReportId Id { get; init; } - public required CrashReportVersion Version { get; init; } - public required GameVersion GameVersion { get; init; } - public required ExceptionTypeId ExceptionType { get; init; } - public required string Exception { get; init; } - public required DateTimeOffset CreatedAt { get; init; } - public required ModuleId[] ModuleIds { get; init; } - public required ModuleIdToVersionView[] ModuleIdToVersion { get; init; } - public required ModuleId? TopInvolvedModuleId { get; init; } - public required ModuleId[] InvolvedModuleIds { get; init; } - public required NexusModsModId[] NexusModsModIds { get; init; } - public required CrashReportUrl Url { get; init; } - public required CrashReportStatus Status { get; init; } - public required string? Comment { get; init; } - } + [HttpPatch] + public async Task> UpdateAsync([FromQuery, Required] CrashReportId crashReportId, [FromQuery] CrashReportStatus? status, [FromQuery] string? comment, [BindUserId] NexusModsUserId userId, [BindTenant] TenantId tenant) + { + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(); - private readonly ILogger _logger; - private readonly IAppDbContextRead _dbContextRead; - private readonly IAppDbContextWrite _dbContextWrite; + var existingEntity = await unitOfWrite.NexusModsUserToCrashReports.FirstOrDefaultAsync( + x => x.TenantId == tenant && x.NexusModsUser.NexusModsUserId == userId && x.CrashReportId == crashReportId, + null, CancellationToken.None); + var entity = new NexusModsUserToCrashReportEntity + { + TenantId = tenant, + NexusModsUserId = userId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(userId), + CrashReportId = crashReportId, + Status = status ?? existingEntity?.Status ?? CrashReportStatus.New, + Comment = comment ?? existingEntity?.Comment ?? string.Empty, + }; + unitOfWrite.NexusModsUserToCrashReports.Upsert(entity); - public CrashReportsController(ILogger logger, IAppDbContextRead dbContextRead, IAppDbContextWrite dbContextWrite) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _dbContextRead = dbContextRead ?? throw new ArgumentNullException(nameof(dbContextRead)); - _dbContextWrite = dbContextWrite ?? throw new ArgumentNullException(nameof(dbContextWrite)); + await unitOfWrite.SaveChangesAsync(CancellationToken.None); + return ApiResult("Updated successful!"); } - private async Task> PaginatedBaseAsync(PaginatedQuery query, NexusModsUserId userId, CancellationToken ct) + private async Task> GetPaginatedBaseAsync(NexusModsUserId userId, PaginatedQuery query, CancellationToken ct) { var page = query.Page; var pageSize = Math.Max(Math.Min(query.PageSize, 50), 5); - var filters = query.Filters ?? Enumerable.Empty(); + var filters = query.Filters ?? []; var sortings = query.Sortings is null || query.Sortings.Count == 0 ? new List { new() { Property = nameof(CrashReportEntity.CreatedAt), Type = SortingType.Descending } } : query.Sortings; - var user = await _dbContextRead.NexusModsUsers - .Include(x => x.ToModules).ThenInclude(x => x.Module) - .Include(x => x.ToNexusModsMods).ThenInclude(x => x.NexusModsMod) - .AsSplitQuery() - .FirstAsync(x => x.NexusModsUserId == userId, ct); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); - IQueryable DbQueryBase(Expression> predicate) - { - return _dbContextRead.CrashReports - .Include(x => x.ToUsers).ThenInclude(x => x.NexusModsUser) - .Include(x => x.ModuleInfos).ThenInclude(x => x.Module) - .Include(x => x.ModuleInfos).ThenInclude(x => x.NexusModsMod) - .Include(x => x.ModuleInfos).ThenInclude(x => x.Module) - .Include(x => x.ExceptionType) - .AsSplitQuery() - .Where(predicate) - .Select(x => new UserCrashReportView - { - Id = x.CrashReportId, - Version = x.Version, - GameVersion = x.GameVersion, - ExceptionType = x.ExceptionType.ExceptionTypeId, - Exception = x.Exception, - CreatedAt = x.CreatedAt, - Url = x.Url, - ModuleIds = x.ModuleInfos.Select(y => y.Module).Select(y => y.ModuleId).ToArray(), - ModuleIdToVersion = x.ModuleInfos.Select(y => new ModuleIdToVersionView { ModuleId = y.Module.ModuleId, Version = y.Version }).ToArray(), - TopInvolvedModuleId = x.ModuleInfos.OrderBy(y => y.InvolvedPosition).Where(z => z.IsInvolved).Select(y => y.Module).Select(y => y.ModuleId).Cast().FirstOrDefault(), - InvolvedModuleIds = x.ModuleInfos.OrderBy(y => y.InvolvedPosition).Where(z => z.IsInvolved).Select(y => y.Module).Select(y => y.ModuleId).ToArray(), - NexusModsModIds = x.ModuleInfos.Select(y => y.NexusModsMod).Where(y => y != null).Select(y => y!.NexusModsModId).ToArray(), - Status = x.ToUsers.Where(y => y.NexusModsUser.NexusModsUserId == userId).Select(y => y.Status).FirstOrDefault(), - Comment = x.ToUsers.Where(y => y.NexusModsUser.NexusModsUserId == userId).Select(y => y.Comment).FirstOrDefault(), - }) - .WithFilter(filters) - .WithSort(sortings); - } - - var moduleIds = user.ToModules.Select(x => x.Module.ModuleId).ToHashSet(); - var nexusModsModIds = user.ToNexusModsMods.Select(x => x.NexusModsMod.NexusModsModId).ToHashSet(); - - var dbQuery = User.IsInRole(ApplicationRoles.Administrator) || User.IsInRole(ApplicationRoles.Moderator) - ? DbQueryBase(x => true) - : DbQueryBase(x => x.ToUsers.Any(y => y.NexusModsUser.NexusModsUserId == userId) || - x.ModuleInfos.Any(y => moduleIds.Contains(y.Module.ModuleId)) || - x.ModuleInfos.Where(y => y.NexusModsMod != null).Any(y => nexusModsModIds.Contains(y.NexusModsMod!.NexusModsModId))); - - return new(await dbQuery.PaginatedAsync(page, pageSize, ct), items => items.Select(x => new CrashReportModel2 + var user = await unitOfRead.NexusModsUsers.GetUserAsync(userId, ct); + return await unitOfRead.CrashReports.GetCrashReportsPaginatedAsync(user!, new PaginatedQuery(page, pageSize, filters, sortings), HttpContext.GetRole(), ct); + } + + [HttpPost("Paginated")] + public async Task?>> GetPaginatedAsync([FromBody, Required] PaginatedQuery query, [BindUserId] NexusModsUserId userId, CancellationToken ct) + { + var paginated = await GetPaginatedBaseAsync(userId, query, ct); + return ApiPagingResult(paginated, items => items.Select(x => new CrashReportModel2 { Id = x.Id, Version = x.Version, @@ -140,48 +100,39 @@ IQueryable DbQueryBase(Expression?>> PaginatedAsync([FromBody] PaginatedQuery query, [BindUserId] NexusModsUserId userId, CancellationToken ct) - { - var (paginated, transform) = await PaginatedBaseAsync(query, userId, ct); - return ApiPagingResult(paginated, transform); - } - [HttpPost("PaginatedStreaming")] [ApiExplorerSettings(IgnoreApi = true)] - public async Task PaginatedStreamingAsync([FromBody] PaginatedQuery query, [BindUserId] NexusModsUserId userId, CancellationToken ct) + public async Task GetPaginatedStreamingAsync([FromBody, Required] PaginatedQuery query, [BindUserId] NexusModsUserId userId, CancellationToken ct) { - var (paginated, transform) = await PaginatedBaseAsync(query, userId, ct); - return ApiPagingStreamingResult(paginated, transform); + var paginated = await GetPaginatedBaseAsync(userId, query, ct); + return ApiPagingStreamingResult(paginated, items => items.Select(x => new CrashReportModel2 + { + Id = x.Id, + Version = x.Version, + GameVersion = x.GameVersion, + ExceptionType = x.ExceptionType, + Exception = x.Exception, + Date = x.CreatedAt, + Url = x.Url, + InvolvedModules = x.InvolvedModuleIds, + Status = x.Status, + Comment = x.Comment ?? string.Empty, + })); } - [HttpGet("Autocomplete")] - public ApiResult?> Autocomplete([FromQuery] ModuleId modId) + [HttpGet("Autocomplete/ModuleIds")] + public async Task?>> GetAutocompleteModuleIdsAsync([FromQuery, Required] ModuleId moduleId) { - return ApiResult(_dbContextRead.AutocompleteStartsWith(x => x.Module.ModuleId, modId)); - } + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); - [HttpPost("Update")] - public async Task> UpdateAsync([FromBody] CrashReportModel2 updatedCrashReport, [BindUserId] NexusModsUserId userId, [BindTenant] TenantId tenant) - { - var entityFactory = _dbContextWrite.GetEntityFactory(); - await using var _ = await _dbContextWrite.CreateSaveScopeAsync(); + var moduleIds = await unitOfRead.Autocompletes.AutocompleteStartsWithAsync(x => x.Module.ModuleId, moduleId, CancellationToken.None); - var entity = new NexusModsUserToCrashReportEntity - { - TenantId = tenant, - NexusModsUser = entityFactory.GetOrCreateNexusModsUser(userId), - CrashReportId = updatedCrashReport.Id, - Status = updatedCrashReport.Status, - Comment = updatedCrashReport.Comment - }; - await _dbContextWrite.NexusModsUserToCrashReports.UpsertOnSaveAsync(entity); - return ApiResult("Updated successful!"); + return ApiResult(moduleIds); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/DiscordController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/DiscordController.cs index e5567966..a650bffd 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/DiscordController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/DiscordController.cs @@ -1,6 +1,6 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Services; using BUTR.Site.NexusMods.Server.Utils; using BUTR.Site.NexusMods.Server.Utils.BindingSources; @@ -8,10 +8,9 @@ using BUTR.Site.NexusMods.Shared.Helpers; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using System; -using System.Linq; +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; @@ -31,26 +30,20 @@ private sealed record Metadata( private readonly IDiscordClient _discordClient; private readonly IDiscordStorage _discordStorage; - private readonly IAppDbContextRead _dbContextRead; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; - public DiscordController(IDiscordClient discordClient, IDiscordStorage discordStorage, IAppDbContextRead dbContextRead) + public DiscordController(IDiscordClient discordClient, IDiscordStorage discordStorage, IUnitOfWorkFactory unitOfWorkFactory) { - _discordClient = discordClient ?? throw new ArgumentNullException(nameof(discordClient)); - _discordStorage = discordStorage ?? throw new ArgumentNullException(nameof(discordStorage)); - _dbContextRead = dbContextRead ?? throw new ArgumentNullException(nameof(dbContextRead)); + _discordClient = discordClient; + _discordStorage = discordStorage; + _unitOfWorkFactory = unitOfWorkFactory; } - [HttpGet("GetOAuthUrl")] - [Produces("application/json")] - public ApiResult GetOAuthUrl() + [HttpPost] + public async Task> AddLinkAsync([FromQuery, Required] string code, [BindRole] ApplicationRole role, CancellationToken ct) { - var (url, state) = _discordClient.GetOAuthUrl(); - return ApiResult(new DiscordOAuthUrlModel(url, state)); - } + var userId = HttpContext.GetUserId(); - [HttpGet("Link")] - public async Task> LinkAsync([FromQuery] string code, [BindRole] ApplicationRole role, [BindUserId] NexusModsUserId userId, CancellationToken ct) - { var tokens = await _discordClient.CreateTokensAsync(code, ct); if (tokens is null) return ApiBadRequest("Failed to link!"); @@ -65,9 +58,11 @@ public DiscordController(IDiscordClient discordClient, IDiscordStorage discordSt return ApiResult("Linked successful!"); } - [HttpPost("Unlink")] - public async Task> UnlinkAsync([BindUserId] NexusModsUserId userId, CancellationToken ct) + [HttpDelete] + public async Task> RemoveLinkAsync(CancellationToken ct) { + var userId = HttpContext.GetUserId(); + var tokens = HttpContext.GetDiscordTokens(); if (tokens?.Data is null) @@ -89,18 +84,29 @@ public DiscordController(IDiscordClient discordClient, IDiscordStorage discordSt return ApiResult("Unlinked successful!"); } - [HttpPost("UpdateMetadata")] - public async Task> UpdateMetadataAsync([BindRole] ApplicationRole role, [BindUserId] NexusModsUserId userId, CancellationToken ct) + [HttpGet("OAuthUrl")] + public ApiResult GetOAuthUrl() { + var (url, state) = _discordClient.GetOAuthUrl(); + return ApiResult(new DiscordOAuthUrlModel(url, state)); + } + + [HttpPut("Metadata")] + public async Task> UpdateMetadataAsync([BindRole] ApplicationRole role, CancellationToken ct) + { + var userId = HttpContext.GetUserId(); + if (await UpdateMetadataInternalAsync(role, userId, ct) is not { } result) return ApiBadRequest("Failed to update"); + return result ? ApiResult("") : ApiBadRequest("Failed to update"); } - - [HttpPost("GetUserInfo")] - public async Task> GetUserInfoByAccessTokenAsync([BindUserId] NexusModsUserId userId, CancellationToken ct) + [HttpGet("UserInfo")] + public async Task> GetUserInfoAsync(CancellationToken ct) { + var userId = HttpContext.GetUserId(); + var tokens = HttpContext.GetDiscordTokens(); if (tokens?.Data is null) @@ -124,12 +130,9 @@ public DiscordController(IDiscordClient discordClient, IDiscordStorage discordSt if (tokens?.Data is null) return null; - var linkedModsCount = await _dbContextRead.NexusModsUsers - .Include(x => x.ToNexusModsMods) - .AsSplitQuery() - .Where(x => x.NexusModsUserId == userId) - .SelectMany(x => x.ToNexusModsMods) - .CountAsync(ct); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(TenantId.None); + + var linkedModsCount = await unitOfRead.NexusModsUsers.GetLinkedModCountAsync(userId, ct); var refreshed = await _discordClient.GetOrRefreshTokensAsync(tokens.Data, ct); if (refreshed is null) diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/ExposedModsController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/ExposedModsController.cs index dc84f736..715f0d0b 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/ExposedModsController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/ExposedModsController.cs @@ -1,16 +1,15 @@ -using BUTR.Site.NexusMods.Server.Contexts; -using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.API; using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Utils; using BUTR.Site.NexusMods.Server.Utils.Http.ApiResults; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using System; -using System.Linq; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; @@ -19,33 +18,32 @@ namespace BUTR.Site.NexusMods.Server.Controllers; [ApiController, Route("api/v1/[controller]"), ButrNexusModsAuthorization, TenantRequired] public class ExposedModsController : ApiControllerBase { - public record ExposedNexusModsModModel(NexusModsModId NexusModsModId, ExposedModuleModel[] Mods); - public record ExposedModuleModel(ModuleId ModuleId, DateTimeOffset LastCheckedDate); - - private readonly ILogger _logger; - private readonly IAppDbContextRead _dbContextRead; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; - public ExposedModsController(ILogger logger, IAppDbContextRead dbContextRead) + public ExposedModsController(ILogger logger, IUnitOfWorkFactory unitOfWorkFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _dbContextRead = dbContextRead ?? throw new ArgumentNullException(nameof(dbContextRead)); + _logger = logger; + _unitOfWorkFactory = unitOfWorkFactory; } [HttpPost("Paginated")] - public async Task?>> PaginatedAsync([FromBody] PaginatedQuery query, CancellationToken ct) + public async Task?>> GetPaginatedAsync([FromBody, Required] PaginatedQuery query, CancellationToken ct) { - var paginated = await _dbContextRead.NexusModsModModules - .Where(x => x.LinkType == NexusModsModToModuleLinkType.ByUnverifiedFileExposure) - .PaginatedAsync(query, 100, new() { Property = nameof(NexusModsModEntity.NexusModsModId), Type = SortingType.Ascending }, ct); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(TenantId.None); + + var paginated = await unitOfRead.NexusModsModModules.GetExposedPaginatedAsync(query, ct); - return ApiPagingResult(paginated, items => items.GroupBy(x => x.NexusModsMod.NexusModsModId).SelectAwaitWithCancellation(async (x, ct2) => - new ExposedNexusModsModModel(x.Key, await x.Select(y => new ExposedModuleModel(y.Module.ModuleId, y.LastUpdateDate.ToUniversalTime())).ToArrayAsync(ct2)))); + return ApiPagingResult(paginated); } - [HttpGet("Autocomplete")] - public ApiResult?> Autocomplete([FromQuery] ModuleId modId) + [HttpGet("Autocomplete/ModuleIds")] + public async Task?>> GetAutocompleteModuleIdsAsync([FromQuery, Required] ModuleId moduleId) { - return ApiResult(_dbContextRead.AutocompleteStartsWith(x => x.Module.ModuleId, modId)); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(TenantId.None); + + var moduleIds = await unitOfRead.Autocompletes.AutocompleteStartsWithAsync(x => x.Module.ModuleId, moduleId, CancellationToken.None); + + return ApiResult(moduleIds); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/GOGController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/GOGController.cs index 78957659..08fc7c7e 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/GOGController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/GOGController.cs @@ -1,13 +1,11 @@ using BUTR.Site.NexusMods.Server.Extensions; -using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Services; using BUTR.Site.NexusMods.Server.Utils; -using BUTR.Site.NexusMods.Server.Utils.BindingSources; using BUTR.Site.NexusMods.Server.Utils.Http.ApiResults; using Microsoft.AspNetCore.Mvc; -using System; +using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; @@ -18,27 +16,22 @@ public sealed class GOGController : ApiControllerBase { public sealed record GOGOAuthUrlModel(string Url); - private readonly IGOGStorage _gogStorage; private readonly IGOGAuthClient _gogAuthClient; private readonly IGOGEmbedClient _gogEmbedClient; public GOGController(IGOGStorage gogStorage, IGOGAuthClient gogAuthClient, IGOGEmbedClient gogEmbedClient) { - _gogStorage = gogStorage ?? throw new ArgumentNullException(nameof(gogStorage)); - _gogAuthClient = gogAuthClient ?? throw new ArgumentNullException(nameof(gogAuthClient)); - _gogEmbedClient = gogEmbedClient ?? throw new ArgumentNullException(nameof(gogEmbedClient)); + _gogStorage = gogStorage; + _gogAuthClient = gogAuthClient; + _gogEmbedClient = gogEmbedClient; } - [HttpGet("GetOAuthUrl")] - public ApiResult GetOpenIdUrl() + [HttpPost] + public async Task> AddLinkAsync([FromQuery, Required] string code, CancellationToken ct) { - return ApiResult(new GOGOAuthUrlModel(_gogAuthClient.GetOAuth2Url())); - } + var userId = HttpContext.GetUserId(); - [HttpGet("Link")] - public async Task> LinkAsync([FromQuery] string code, [BindUserId] NexusModsUserId userId, CancellationToken ct) - { var tokens = await _gogAuthClient.CreateTokensAsync(code, ct); if (tokens is null) return ApiBadRequest("Failed to link!"); @@ -52,9 +45,11 @@ public GOGController(IGOGStorage gogStorage, IGOGAuthClient gogAuthClient, IGOGE return ApiResult("Linked successful!"); } - [HttpPost("Unlink")] - public async Task> UnlinkAsync([BindUserId] NexusModsUserId userId) + [HttpDelete] + public async Task> RemoveLinkAsync() { + var userId = HttpContext.GetUserId(); + var tokens = HttpContext.GetGOGTokens(); if (tokens?.Data is null) @@ -66,9 +61,17 @@ public GOGController(IGOGStorage gogStorage, IGOGAuthClient gogAuthClient, IGOGE return ApiResult("Unlinked successful!"); } - [HttpPost("GetUserInfo")] - public async Task> GetUserInfoByAccessTokenAsync([BindUserId] NexusModsUserId userId, CancellationToken ct) + [HttpGet("OAuthUrl")] + public ApiResult GetOAuthUrl() { + return ApiResult(new GOGOAuthUrlModel(_gogAuthClient.GetOAuth2Url())); + } + + [HttpGet("UserInfo")] + public async Task> GetUserInfoAsync(CancellationToken ct) + { + var userId = HttpContext.GetUserId(); + var tokens = HttpContext.GetGOGTokens(); if (tokens?.Data is null) diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/GamePublicApiDiffController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/GamePublicApiDiffController.cs index de9f40cb..9d98e070 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/GamePublicApiDiffController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/GamePublicApiDiffController.cs @@ -22,8 +22,8 @@ public sealed class GamePublicApiDiffController : ApiControllerBase public GamePublicApiDiffController(ILogger logger, IDiffProvider diffProvider) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _diffProvider = diffProvider ?? throw new ArgumentNullException(nameof(diffProvider)); + _logger = logger; + _diffProvider = diffProvider; } [HttpGet("List")] diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/GameSourceDiffController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/GameSourceDiffController.cs index d839f63f..b1cb86cd 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/GameSourceDiffController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/GameSourceDiffController.cs @@ -24,8 +24,8 @@ public sealed class GameSourceDiffController : ApiControllerBase public GameSourceDiffController(ILogger logger, IDiffProvider diffProvider) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _diffProvider = diffProvider ?? throw new ArgumentNullException(nameof(diffProvider)); + _logger = logger; + _diffProvider = diffProvider; } [HttpGet("List")] diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/GitHubController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/GitHubController.cs index 57446c99..bb48d138 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/GitHubController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/GitHubController.cs @@ -1,18 +1,12 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; -using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Services; using BUTR.Site.NexusMods.Server.Utils; -using BUTR.Site.NexusMods.Server.Utils.BindingSources; using BUTR.Site.NexusMods.Server.Utils.Http.ApiResults; -using BUTR.Site.NexusMods.Shared.Helpers; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using System; -using System.Linq; -using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; @@ -29,22 +23,16 @@ public sealed record GitHubOAuthUrlModel(string Url, Guid State); public GitHubController(IGitHubClient gitHubClient, IGitHubAPIClient gitHubApiClient, IGitHubStorage gitHubStorage) { - _gitHubClient = gitHubClient ?? throw new ArgumentNullException(nameof(gitHubClient)); - _gitHubApiClient = gitHubApiClient ?? throw new ArgumentNullException(nameof(gitHubApiClient)); - _gitHubStorage = gitHubStorage ?? throw new ArgumentNullException(nameof(gitHubStorage)); + _gitHubClient = gitHubClient; + _gitHubApiClient = gitHubApiClient; + _gitHubStorage = gitHubStorage; } - [HttpGet("GetOAuthUrl")] - [Produces("application/json")] - public ApiResult GetOAuthUrl() + [HttpPost] + public async Task> AddLinkAsync([FromQuery, Required] string code, CancellationToken ct) { - var (url, state) = _gitHubClient.GetOAuthUrl(); - return ApiResult(new GitHubOAuthUrlModel(url, state)); - } + var userId = HttpContext.GetUserId(); - [HttpGet("Link")] - public async Task> LinkAsync([FromQuery] string code, [BindRole] ApplicationRole role, [BindUserId] NexusModsUserId userId, CancellationToken ct) - { var tokens = await _gitHubClient.CreateTokensAsync(code, ct); if (tokens is null) return ApiBadRequest("Failed to link!"); @@ -57,9 +45,11 @@ public GitHubController(IGitHubClient gitHubClient, IGitHubAPIClient gitHubApiCl return ApiResult("Linked successful!"); } - [HttpPost("Unlink")] - public async Task> UnlinkAsync([BindUserId] NexusModsUserId userId, CancellationToken ct) + [HttpDelete] + public async Task> RemoveLinkAsync(CancellationToken ct) { + var userId = HttpContext.GetUserId(); + var tokens = HttpContext.GetDiscordTokens(); if (tokens?.Data is null) @@ -78,8 +68,15 @@ public GitHubController(IGitHubClient gitHubClient, IGitHubAPIClient gitHubApiCl return ApiResult("Unlinked successful!"); } - [HttpPost("GetUserInfo")] - public async Task> GetUserInfoByAccessTokenAsync([BindUserId] NexusModsUserId userId, CancellationToken ct) + [HttpGet("OAuthUrl")] + public ApiResult GetOAuthUrl() + { + var (url, state) = _gitHubClient.GetOAuthUrl(); + return ApiResult(new GitHubOAuthUrlModel(url, state)); + } + + [HttpGet("UserInfo")] + public async Task> GetUserInfoAsync(CancellationToken ct) { var tokens = HttpContext.GetGitHubTokens(); diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/ModsAnalyzerController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/ModsAnalyzerController.cs index 392f7c51..969dbbd3 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/ModsAnalyzerController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/ModsAnalyzerController.cs @@ -1,17 +1,16 @@ using Bannerlord.ModuleManager; -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Utils; using BUTR.Site.NexusMods.Server.Utils.BindingSources; -using BUTR.Site.NexusMods.Server.Utils.Http.ApiResults; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; @@ -33,7 +32,6 @@ public sealed record CompatibilityScoreRequest public required ICollection Modules { get; init; } } - public sealed record CompatibilityScoreResultModuleEntry { public required ModuleId ModuleId { get; init; } @@ -49,23 +47,21 @@ public sealed record CompatibilityScoreResult private readonly ILogger _logger; - private readonly IAppDbContextRead _dbContextRead; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; - public ModsAnalyzerController(ILogger logger, IAppDbContextRead dbContextRead) + public ModsAnalyzerController(ILogger logger, IUnitOfWorkFactory unitOfWorkFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _dbContextRead = dbContextRead ?? throw new ArgumentNullException(nameof(dbContextRead)); + _logger = logger; + _unitOfWorkFactory = unitOfWorkFactory; } - [HttpPost("GetCompatibilityScore")] + [HttpPost("CompatibilityScores")] [ResponseCache(Duration = 60 * 60 * 2)] - public async Task> GetCompatibilityScoreAsync([BindTenant] TenantId tenant, [FromBody] CompatibilityScoreRequest crashReport, [FromServices] ITenantContextAccessor tenantContextAccessor, CancellationToken ct) + public async Task> GetCompatibilityScoreAsync([FromBody, Required] CompatibilityScoreRequest crashReport, [BindTenant] TenantId tenant, CancellationToken ct) { - tenantContextAccessor.Current = tenant; - if (tenant == TenantId.Bannerlord) { - var toExclude = new HashSet() + var toExclude = new HashSet { ModuleId.From("Native"), ModuleId.From("SandBoxCore"), @@ -114,21 +110,10 @@ private async IAsyncEnumerable GetCompatibi var gameVersion = data.GameVersion; var currentModules = data.Modules; + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); + var moduleIds = currentModules.Select(x => x.ModuleId).ToArray(); - var allRawScoresForAllModules = await _dbContextRead.StatisticsCrashScoreInvolveds - .Where(x => x.GameVersion == gameVersion && moduleIds.Contains(x.Module.ModuleId)) - .GroupBy(x => x.Module.ModuleId) - .Select(x => new - { - ModuleId = x.Key, - // We order by score so that we can take the top 10 - RawScores = x.OrderBy(y => y.Score).Select(y => new - { - ModuleVersion = y.ModuleVersion, - RawScore = y.Score, - TotalCount = y.TotalCount, - }).Take(10).ToArray(), - }).ToArrayAsync(ct); + var allRawScoresForAllModules = await unitOfRead.StatisticsCrashScoreInvolveds.GetAllRawScoresForAllModulesAsync(gameVersion, moduleIds, ct); var allScoresForAllModules = allRawScoresForAllModules.Select(x => { diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsArticleController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsArticleController.cs index 08a9c380..271a057f 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsArticleController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsArticleController.cs @@ -1,17 +1,16 @@ -using BUTR.Site.NexusMods.Server.Contexts; -using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.API; -using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Utils; +using BUTR.Site.NexusMods.Server.Utils.BindingSources; using BUTR.Site.NexusMods.Server.Utils.Http.ApiResults; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; -using System.Linq; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; @@ -20,46 +19,51 @@ namespace BUTR.Site.NexusMods.Server.Controllers; [ApiController, Route("api/v1/[controller]"), ButrNexusModsAuthorization, TenantRequired] public class NexusModsArticleController : ApiControllerBase { - public record NexusModsArticleModel(NexusModsArticleId NexusModsArticleId, string Title, NexusModsUserId NexusModsUserId, NexusModsUserName Author, DateTimeOffset CreateDate); + public sealed record NexusModsArticleModel + { + public NexusModsArticleId NexusModsArticleId { get; init; } + public string Title { get; init; } + public NexusModsUserId NexusModsUserId { get; init; } + public NexusModsUserName Author { get; init; } + public DateTimeOffset CreateDate { get; init; } + } private readonly ILogger _logger; - private readonly IAppDbContextRead _dbContextRead; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; - public NexusModsArticleController(ILogger logger, IAppDbContextRead dbContextRead) + public NexusModsArticleController(ILogger logger, IUnitOfWorkFactory unitOfWorkFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _dbContextRead = dbContextRead ?? throw new ArgumentNullException(nameof(dbContextRead)); + _logger = logger; + _unitOfWorkFactory = unitOfWorkFactory; } [HttpPost("Paginated")] - public async Task?>> PaginatedAsync([FromBody] PaginatedQuery query, CancellationToken ct) + public async Task?>> GetPaginatedAsync([FromBody, Required] PaginatedQuery query, CancellationToken ct) { - var paginated = await _dbContextRead.NexusModsArticles - .Include(x => x.NexusModsUser).ThenInclude(x => x.Name) - .Select(x => new + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); + + var paginated = await unitOfRead.NexusModsArticles.PaginatedAsync(x => + new NexusModsArticleModel { NexusModsArticleId = x.NexusModsArticleId, Title = x.Title, NexusModsUserId = x.NexusModsUser.NexusModsUserId, Author = x.NexusModsUser.Name != null ? x.NexusModsUser.Name.Name : NexusModsUserName.Empty, - CreateDate = x.CreateDate, - }) - .PaginatedAsync(query, 100, new() { Property = nameof(NexusModsArticleEntity.NexusModsArticleId), Type = SortingType.Ascending }, ct); + CreateDate = x.CreateDate + }, + query, 100, new() { Property = nameof(NexusModsArticleModel.NexusModsArticleId), Type = SortingType.Ascending }, ct); - return ApiPagingResult(paginated, items => items.Select(x => new NexusModsArticleModel(x.NexusModsArticleId, x.Title, x.NexusModsUserId, x.Author, x.CreateDate))); + return ApiPagingResult(paginated); } - [HttpGet("Autocomplete")] - public ApiResult?> Autocomplete([FromQuery] string authorName) + [HttpGet("Autocompletes/AuthorNames")] + public async Task?>> GetAutocompleteAuthorNamesAsync([FromQuery, Required] string authorName, [BindTenant] TenantId tenant, CancellationToken ct) { - var moduleIds = _dbContextRead.NexusModsArticles - .Include(x => x.NexusModsUser).ThenInclude(x => x.Name) - .Select(x => x.NexusModsUser) - .Select(x => x.Name!) - .Where(x => EF.Functions.ILike(x.Name.Value, $"{authorName}%")) - .Select(x => x.Name.Value) - .Distinct(); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(tenant); + + var moduleIds = await unitOfRead.NexusModsArticles.GetAllModuleIdsAsync(authorName, ct); + // TODO: return ApiResult(moduleIds); diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsModController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsModController.cs index cff39ef0..6b8ea913 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsModController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsModController.cs @@ -1,8 +1,8 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.API; using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Services; using BUTR.Site.NexusMods.Server.Utils; using BUTR.Site.NexusMods.Server.Utils.BindingSources; @@ -10,11 +10,10 @@ using BUTR.Site.NexusMods.Shared.Helpers; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; -using System.Linq; +using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; @@ -25,30 +24,31 @@ public sealed class NexusModsModController : ApiControllerBase { public sealed record RawNexusModsModModel(NexusModsModId NexusModsModId, string Name); - public sealed record NexusModsModToModuleModel(ModuleId ModuleId, NexusModsModId NexusModsModId); - - public sealed record NexusModsModToModuleManualLinkQuery(ModuleId ModuleId, NexusModsModId NexusModsModId); - - public sealed record NexusModsModAvailableModel(NexusModsModId NexusModsModId, string Name); - private readonly ILogger _logger; private readonly INexusModsAPIClient _nexusModsAPIClient; - private readonly IAppDbContextRead _dbContextRead; - private readonly IAppDbContextWrite _dbContextWrite; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; - public NexusModsModController(ILogger logger, INexusModsAPIClient nexusModsAPIClient, IAppDbContextRead dbContextRead, IAppDbContextWrite dbContextWrite) + public NexusModsModController(ILogger logger, INexusModsAPIClient nexusModsAPIClient, IUnitOfWorkFactory unitOfWorkFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _nexusModsAPIClient = nexusModsAPIClient ?? throw new ArgumentNullException(nameof(nexusModsAPIClient)); - _dbContextRead = dbContextRead ?? throw new ArgumentNullException(nameof(dbContextRead)); - _dbContextWrite = dbContextWrite ?? throw new ArgumentNullException(nameof(dbContextWrite)); + _logger = logger; + _nexusModsAPIClient = nexusModsAPIClient; + _unitOfWorkFactory = unitOfWorkFactory; } + [HttpGet("Raws")] + public async Task> GetRawAsync([FromQuery, Required] NexusModsModId modId, [BindUserId] NexusModsUserId userId, [BindTenant] TenantId tenant, CancellationToken ct) + { + if (HttpContext.GetAPIKey() is { } apiKey && apiKey != NexusModsApiKey.None) + return await GetRawWithApiKeyAsync(apiKey, modId, userId, tenant, ct); - [HttpGet("Raw/{gameDomain}/{modId}")] - public async Task> RawAsync(NexusModsGameDomain gameDomain, NexusModsModId modId, [BindApiKey] NexusModsApiKey apiKey, [BindUserId] NexusModsUserId userId, CancellationToken ct) + // TODO: + return ApiBadRequest("Token auth not supported yet!"); + } + private async Task> GetRawWithApiKeyAsync(NexusModsApiKey apiKey, NexusModsModId modId, NexusModsUserId userId, TenantId tenant, CancellationToken ct) { + var gameDomain = tenant.ToGameDomain(); + if (await _nexusModsAPIClient.GetModAsync(gameDomain, modId, apiKey, ct) is not { } modInfo) return ApiBadRequest("Mod not found!"); @@ -59,67 +59,49 @@ public NexusModsModController(ILogger logger, INexusMods } - [HttpGet("ToModuleManualLink")] + [HttpPost("ModuleManualLinks")] [ButrNexusModsAuthorization(Roles = $"{ApplicationRoles.Administrator},{ApplicationRoles.Moderator}")] - public async Task> ToModuleManualLinkAsync([FromQuery] NexusModsModToModuleManualLinkQuery query, [BindTenant] TenantId tenant) + public async Task> AddModuleManualLinkAsync([FromQuery, Required] NexusModsModId modId, [FromQuery, Required] ModuleId moduleId, [BindTenant] TenantId tenant) { - await using var _ = await _dbContextWrite.CreateSaveScopeAsync(); - var entityFactory = _dbContextWrite.GetEntityFactory(); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(); var nexusModsModToModule = new NexusModsModToModuleEntity { TenantId = tenant, - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(query.NexusModsModId), - Module = entityFactory.GetOrCreateModule(query.ModuleId), + NexusModsModId = modId, + NexusModsMod = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modId), + ModuleId = moduleId, + Module = unitOfWrite.UpsertEntityFactory.GetOrCreateModule(moduleId), LinkType = NexusModsModToModuleLinkType.ByStaff, LastUpdateDate = DateTimeOffset.UtcNow, }; - await _dbContextWrite.NexusModsModModules.UpsertOnSaveAsync(nexusModsModToModule); + unitOfWrite.NexusModsModModules.Upsert(nexusModsModToModule); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return ApiResult("Linked successful!"); } - [HttpGet("ToModuleManualUnlink")] + [HttpDelete("ModuleManualLinks")] [ButrNexusModsAuthorization(Roles = $"{ApplicationRoles.Administrator},{ApplicationRoles.Moderator}")] - public async Task> ToModuleManualUnlinkAsync([FromQuery] NexusModsModToModuleManualLinkQuery query) + public async Task> RemoveModuleManualLinkAsync([FromQuery, Required] NexusModsModId modId, [FromQuery, Required] ModuleId moduleId) { - await _dbContextWrite.NexusModsModModules - .Where(x => x.Module.ModuleId == query.ModuleId && x.NexusModsMod.NexusModsModId == query.NexusModsModId && x.LinkType == NexusModsModToModuleLinkType.ByStaff) - .ExecuteDeleteAsync(); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(); + + unitOfWrite.NexusModsModModules + .Remove(x => x.Module.ModuleId == moduleId && x.NexusModsMod.NexusModsModId == modId && x.LinkType == NexusModsModToModuleLinkType.ByStaff); - return ApiBadRequest("Failed to unlink!"); + await unitOfWrite.SaveChangesAsync(CancellationToken.None); + return ApiResult("Unlinked successful!"); } - [HttpPost("ToModuleManualLinkPaginated")] + [HttpPost("ModuleManualLinks/Paginated")] [ButrNexusModsAuthorization(Roles = $"{ApplicationRoles.Administrator},{ApplicationRoles.Moderator}")] - public async Task?>> ToModuleManualLinkPaginatedAsync([FromBody] PaginatedQuery query, CancellationToken ct) + public async Task?>> GetModuleManualLinkPaginatedAsync([FromBody, Required] PaginatedQuery query, CancellationToken ct) { - var paginated = await _dbContextRead.NexusModsModModules - .Where(x => x.LinkType == NexusModsModToModuleLinkType.ByStaff) - .PaginatedAsync(query, 20, new() { Property = nameof(ModuleEntity.ModuleId), Type = SortingType.Ascending }, ct); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); - return ApiPagingResult(paginated, items => items.Select(m => new NexusModsModToModuleModel(m.Module.ModuleId, m.NexusModsMod.NexusModsModId))); - } + var paginated = await unitOfRead.NexusModsModModules.GetByStaffPaginatedAsync(query, ct); - - [HttpPost("AvailablePaginated")] - public async Task?>> AvailablePaginatedAsync([FromBody] PaginatedQuery query, [BindUserId] NexusModsUserId userId, CancellationToken ct) - { - var userToModIds = _dbContextRead.NexusModsUserToNexusModsMods - .Include(x => x.NexusModsMod).ThenInclude(x => x.Name) - .Where(x => x.NexusModsUser.NexusModsUserId == userId) - .Select(x => x.NexusModsMod); - - var userToModuleIdsToModIds = _dbContextRead.NexusModsUserToModules - .Include(x => x.Module).ThenInclude(x => x.ToNexusModsMods).ThenInclude(x => x.NexusModsMod).ThenInclude(x => x.Name) - .AsSplitQuery() - .Where(x => x.NexusModsUser.NexusModsUserId == userId) - .Select(x => x.Module) - .SelectMany(x => x.ToNexusModsMods) - .Select(x => x.NexusModsMod); - - var paginated = await userToModIds.Union(userToModuleIdsToModIds) - .PaginatedAsync(query, 20, new() { Property = nameof(NexusModsModEntity.NexusModsModId), Type = SortingType.Ascending }, ct); - - return ApiPagingResult(paginated, items => items.Select(x => new NexusModsModAvailableModel(x.NexusModsModId, x.Name!.Name))); + return ApiPagingResult(paginated); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsUserController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsUserController.cs index 0eee668e..f685e894 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsUserController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/NexusModsUserController.cs @@ -1,8 +1,8 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.API; using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Services; using BUTR.Site.NexusMods.Server.Utils; using BUTR.Site.NexusMods.Server.Utils.BindingSources; @@ -10,11 +10,10 @@ using BUTR.Site.NexusMods.Shared.Helpers; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; -using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -24,288 +23,384 @@ namespace BUTR.Site.NexusMods.Server.Controllers; [ApiController, Route("api/v1/[controller]"), ButrNexusModsAuthorization, TenantRequired] public sealed class NexusModsUserController : ApiControllerBase { - public sealed record SetRoleBody(NexusModsUserId NexusModsUserId, ApplicationRole Role); - - public sealed record RemoveRoleBody(NexusModsUserId NexusModsUserId); - - public sealed record NexusModsUserToNexusModsModQuery(NexusModsModId NexusModsModId); - - public sealed record NexusModsUserToModuleQuery(NexusModsUserId NexusModsUserId, ModuleId ModuleId); - - public sealed record NexusModsUserToNexusModsModManualLinkQuery(NexusModsUserId NexusModsUserId, NexusModsModId NexusModsModId); - - public sealed record NexusModsUserToModuleManualLinkModel(NexusModsUserId NexusModsUserId, ImmutableArray AllowedModuleIds); - - public sealed record NexusModsUserToNexusModsModManualLinkModel(NexusModsModId NexusModsModId, ImmutableArray AllowedNexusModsUserIds); - - public sealed record NexusModsModModel( - NexusModsModId NexusModsModId, - string Name, - ImmutableArray AllowedNexusModsUserIds, - ImmutableArray ManuallyLinkedNexusModsUserIds, - ImmutableArray ManuallyLinkedModuleIds, - ImmutableArray KnownModuleIds); - private readonly ILogger _logger; private readonly INexusModsAPIClient _nexusModsAPIClient; + private readonly INexusModsAPIv2Client _nexusModsAPIv2Client; private readonly INexusModsModFileParser _nexusModsModFileParser; - private readonly IAppDbContextWrite _dbContextWrite; - private readonly IAppDbContextRead _dbContextRead; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; - public NexusModsUserController(ILogger logger, INexusModsAPIClient nexusModsAPIClient, INexusModsModFileParser nexusModsModFileParser, IAppDbContextWrite dbContextWrite, IAppDbContextRead dbContextRead) + public NexusModsUserController(ILogger logger, INexusModsAPIClient nexusModsAPIClient, INexusModsAPIv2Client nexusModsAPIv2Client, INexusModsModFileParser nexusModsModFileParser, IUnitOfWorkFactory unitOfWorkFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _nexusModsAPIClient = nexusModsAPIClient ?? throw new ArgumentNullException(nameof(nexusModsAPIClient)); - _nexusModsModFileParser = nexusModsModFileParser ?? throw new ArgumentNullException(nameof(nexusModsModFileParser)); - _dbContextWrite = dbContextWrite ?? throw new ArgumentNullException(nameof(dbContextWrite)); - _dbContextRead = dbContextRead ?? throw new ArgumentNullException(nameof(dbContextRead)); + _logger = logger; + _nexusModsAPIClient = nexusModsAPIClient; + _nexusModsAPIv2Client = nexusModsAPIv2Client; + _nexusModsModFileParser = nexusModsModFileParser; + _unitOfWorkFactory = unitOfWorkFactory; } + private async Task GetUserIdAsync(NexusModsUserId? userId, NexusModsUserName? username, CancellationToken ct) => userId switch + { + null when username is not null => await _nexusModsAPIv2Client.GetUserIdAsync(HttpContext.GetAPIKey(), username.Value, ct), + null when username is null => HttpContext.GetUserId(), + _ => NexusModsUserId.None + }; [HttpGet("Profile")] - public ApiResult Profile() => ApiResult(HttpContext.GetProfile()); + public ApiResult GetProfile() => ApiResult(HttpContext.GetProfile()); - [HttpPost("SetRole")] + [HttpPatch("Role")] [ButrNexusModsAuthorization(Roles = $"{ApplicationRoles.Administrator},{ApplicationRoles.Moderator}")] - public async Task> SetRoleAsync([FromQuery] SetRoleBody body, [BindTenant] TenantId tenant, CancellationToken ct) + public async Task> SetRoleAsync([FromQuery, Required] ApplicationRole role, [FromQuery] NexusModsUserId? userId, [FromQuery] NexusModsUserName? username, [BindTenant] TenantId tenant, CancellationToken ct) { - await using var _ = await _dbContextWrite.CreateSaveScopeAsync(); + var nexusModsUserId = await GetUserIdAsync(userId, username, ct); + if (nexusModsUserId == NexusModsUserId.None) + return ApiBadRequest("User not found!"); + + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(); + + unitOfWrite.NexusModsUsers.Upsert(NexusModsUserEntity.CreateWithRole(nexusModsUserId, tenant, role)); - await _dbContextWrite.NexusModsUsers.UpsertOnSaveAsync(NexusModsUserEntity.CreateWithRole(body.NexusModsUserId, tenant, body.Role)); + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return ApiResult("Set successful!"); } - [HttpDelete("RemoveRole")] + [HttpDelete("Role")] [ButrNexusModsAuthorization(Roles = $"{ApplicationRoles.Administrator},{ApplicationRoles.Moderator}")] - public async Task> RemoveRoleAsync([FromQuery] RemoveRoleBody body, [BindTenant] TenantId tenant, CancellationToken ct) + public async Task> RemoveRoleAsync([FromQuery] NexusModsUserId? userId, [FromQuery] NexusModsUserName? username, [BindTenant] TenantId tenant, CancellationToken ct) { - await using var _ = await _dbContextWrite.CreateSaveScopeAsync(); + var nexusModsUserId = await GetUserIdAsync(userId, username, ct); + if (nexusModsUserId == NexusModsUserId.None) + return ApiBadRequest("User not found!"); - await _dbContextWrite.NexusModsUsers.UpsertOnSaveAsync(NexusModsUserEntity.CreateWithRole(body.NexusModsUserId, tenant, ApplicationRole.User)); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(); + + unitOfWrite.NexusModsUsers.Upsert(NexusModsUserEntity.CreateWithRole(nexusModsUserId, tenant, ApplicationRole.User)); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return ApiResult("Removed successful!"); } - [HttpPost("ToNexusModsModPaginated")] - public async Task?>> ToNexusModsModPaginatedAsync([FromBody] PaginatedQuery query, [BindUserId] NexusModsUserId userId, CancellationToken ct) + + [HttpPost("NexusModsMods/Paginated")] + public async Task?>> GetNexusModsModsPaginatedAsync([FromBody, Required] PaginatedQuery query, CancellationToken ct) { - var availableModsByNexusModsModLinkage = _dbContextRead.NexusModsUsers - .Include(x => x.ToNexusModsMods).ThenInclude(x => x.NexusModsMod).ThenInclude(x => x.ToNexusModsUsers).ThenInclude(x => x.NexusModsUser) - .AsSplitQuery() - .Where(x => x.NexusModsUserId == userId) - .SelectMany(x => x.ToNexusModsMods) - .Select(x => x.NexusModsMod) - .Select(x => new - { - NexusModsModId = x.NexusModsModId, - Name = x.Name!.Name, - OwnerNexusModsUserIds = x.ToNexusModsUsers.Where(y => y.NexusModsUser.NexusModsUserId != userId && y.LinkType == NexusModsUserToNexusModsModLinkType.ByAPIConfirmation).Select(y => y.NexusModsUser.NexusModsUserId).ToArray(), - AllowedNexusModsUserIds = x.ToNexusModsUsers.Where(y => y.NexusModsUser.NexusModsUserId != userId && y.LinkType == NexusModsUserToNexusModsModLinkType.ByOwner || y.LinkType == NexusModsUserToNexusModsModLinkType.ByStaff).Select(y => y.NexusModsUser.NexusModsUserId).ToArray(), - NexusModsUserIds = x.ToNexusModsUsers.Where(y => y.NexusModsUser.NexusModsUserId != userId && y.LinkType == NexusModsUserToNexusModsModLinkType.ByOwner).Select(y => y.NexusModsUser.NexusModsUserId).ToArray(), - LinkedModuleIds = x.ModuleIds.Where(y => y.LinkType == NexusModsModToModuleLinkType.ByStaff).Select(y => y.Module.ModuleId).ToArray(), - KnownModuleIds = x.ModuleIds.Where(y => y.LinkType == NexusModsModToModuleLinkType.ByUnverifiedFileExposure).Select(y => y.Module.ModuleId).ToArray(), - }); - - var paginated = await availableModsByNexusModsModLinkage - .PaginatedAsync(query, 20, new() { Property = nameof(NexusModsModEntity.NexusModsModId), Type = SortingType.Ascending }, ct); - - return ApiPagingResult(paginated, items => items.Select(m => new NexusModsModModel( - NexusModsModId: m.NexusModsModId, - Name: m.Name, - AllowedNexusModsUserIds: m.AllowedNexusModsUserIds.AsImmutableArray(), - ManuallyLinkedNexusModsUserIds: m.NexusModsUserIds.AsImmutableArray(), - ManuallyLinkedModuleIds: m.LinkedModuleIds.AsImmutableArray(), - KnownModuleIds: m.KnownModuleIds.AsImmutableArray()))); + var userId = HttpContext.GetUserId(); + + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); + + var paginated = await unitOfRead.NexusModsUsers.GetNexusModsModsPaginatedAsync(userId, query, ct); + + return ApiPagingResult(paginated); } - [HttpPost("ToNexusModsModUpdate")] - public async Task> ToNexusModsModUpdateAsync([FromBody] NexusModsUserToNexusModsModQuery query, [BindApiKey] NexusModsApiKey apiKey, [BindUserId] NexusModsUserId userId, [BindTenant] TenantId tenant, CancellationToken ct) + [HttpPost("NexusModsMods/Available/Paginated")] + public async Task?>> GetNexusModsModsPaginateAvailabledAsync([FromBody, Required] PaginatedQuery query, CancellationToken ct) { - await using var _ = await _dbContextWrite.CreateSaveScopeAsync(); - var entityFactory = _dbContextWrite.GetEntityFactory(); + var userId = HttpContext.GetUserId(); - var gameDomain = tenant.ToGameDomain(); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); - if (await _nexusModsAPIClient.GetModAsync(gameDomain, query.NexusModsModId, apiKey, ct) is not { } modInfo) - return ApiBadRequest("Mod not found!"); + var paginated = await unitOfRead.NexusModsUsers.GetAvailableModsPaginatedAsync(userId, query, ct); - if (userId != modInfo.User.Id) - return ApiBadRequest("User does not have access to the mod!"); - - var entity = new NexusModsModToNameEntity - { - TenantId = tenant, - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(query.NexusModsModId), - Name = modInfo.Name, - }; - await _dbContextWrite.NexusModsModName.UpsertOnSaveAsync(entity); - return ApiResult("Updated successful!"); + return ApiPagingResult(paginated); } - [HttpGet("ToNexusModsModLink")] - public async Task> ToNexusModsModLinkAsync([FromQuery] NexusModsUserToNexusModsModQuery query, [BindApiKey] NexusModsApiKey apiKey, [BindUserId] NexusModsUserId userId, [BindTenant] TenantId tenant, CancellationToken ct) + [HttpPost("NexusModsModLinks")] + public async Task> AddNexusModsModLinkAsync([FromQuery, Required] NexusModsModId modId, [FromQuery] NexusModsUserId? userId, [FromQuery] NexusModsUserName? username, [BindTenant] TenantId tenant, CancellationToken ct) { - await using var _ = await _dbContextWrite.CreateSaveScopeAsync(); - var entityFactory = _dbContextWrite.GetEntityFactory(); + var nexusModsUserId = await GetUserIdAsync(userId, username, ct); + if (nexusModsUserId == NexusModsUserId.None) + return ApiBadRequest("User not found!"); + var currentUserId = HttpContext.GetUserId(); + if (currentUserId != nexusModsUserId && HttpContext.GetRole() != ApplicationRoles.Moderator && HttpContext.GetRole() != ApplicationRoles.Administrator) + return ApiBadRequest("Permission denied!"); + + if (HttpContext.GetAPIKey() is { } apiKey && apiKey != NexusModsApiKey.None) + return await AddNexusModsModLinkWithApiKeyAsync(apiKey, modId, nexusModsUserId, tenant, ct); + + // TODO: + return ApiBadRequest("Token auth not supported yet!"); + } + private async Task> AddNexusModsModLinkWithApiKeyAsync(NexusModsApiKey apiKey, NexusModsModId modId, NexusModsUserId userId, TenantId tenant, CancellationToken ct) + { var gameDomain = tenant.ToGameDomain(); - if (await _nexusModsAPIClient.GetModAsync(gameDomain, query.NexusModsModId, apiKey, ct) is not { } modInfo) + if (await _nexusModsAPIClient.GetModAsync(gameDomain, modId, apiKey, ct) is not { } modInfo) return ApiBadRequest("Mod not found!"); if (userId != modInfo.User.Id) return ApiBadRequest("User does not have access to the mod!"); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(); + if (HttpContext.GetIsPremium()) // Premium is needed for API based downloading { var response = await _nexusModsAPIClient.GetModFileInfosFullAsync(gameDomain, modInfo.Id, apiKey, ct); if (response is null) return ApiBadRequest("Error while fetching the mod!"); - var infos = await _nexusModsModFileParser.GetModuleInfosAsync(gameDomain, modInfo.Id, response.Files, apiKey, ct).ToImmutableArrayAsync(ct); - - var entities = infos.Select(x => x.ModuleInfo).DistinctBy(x => x.Id).Select(y => new NexusModsModToModuleEntity - { - TenantId = tenant, - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(query.NexusModsModId), - Module = entityFactory.GetOrCreateModule(ModuleId.From(y.Id)), - LinkType = NexusModsModToModuleLinkType.ByUnverifiedFileExposure, - LastUpdateDate = DateTimeOffset.UtcNow - }).ToArray(); - await _dbContextWrite.NexusModsModModules.UpsertOnSaveAsync(entities); + var entities = await _nexusModsModFileParser.GetModuleInfosAsync(gameDomain, modInfo.Id, response.Files, apiKey, ct) + .Select(x => x.ModuleInfo) + .GroupBy(x => x.Id) + .SelectAwaitWithCancellation(async (x, ct2) => await x.FirstOrDefaultAsync(ct2)) + .Select(y => new NexusModsModToModuleEntity + { + TenantId = tenant, + NexusModsModId = modId, + NexusModsMod = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modId), + ModuleId = ModuleId.From(y.Id), + Module = unitOfWrite.UpsertEntityFactory.GetOrCreateModule(ModuleId.From(y.Id)), + LinkType = NexusModsModToModuleLinkType.ByUnverifiedFileExposure, + LastUpdateDate = DateTimeOffset.UtcNow + }).ToArrayAsync(ct); + unitOfWrite.NexusModsModModules.UpsertRange(entities); } var nexusModsModToName = new NexusModsModToNameEntity { TenantId = tenant, - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(query.NexusModsModId), + NexusModsModId = modId, + NexusModsMod = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modId), Name = modInfo.Name, }; - await _dbContextWrite.NexusModsModName.UpsertOnSaveAsync(nexusModsModToName); + unitOfWrite.NexusModsModName.Upsert(nexusModsModToName); + var nexusModsUserToNexusModsMod = new NexusModsUserToNexusModsModEntity { TenantId = tenant, - NexusModsUser = entityFactory.GetOrCreateNexusModsUser(userId), - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(query.NexusModsModId), + NexusModsUserId = userId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(userId), + NexusModsModId = modId, + NexusModsMod = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modId), LinkType = NexusModsUserToNexusModsModLinkType.ByAPIConfirmation, }; - await _dbContextWrite.NexusModsUserToNexusModsMods.UpsertOnSaveAsync(nexusModsUserToNexusModsMod); + unitOfWrite.NexusModsUserToNexusModsMods.Upsert(nexusModsUserToNexusModsMod); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return ApiResult("Linked successful!"); } - [HttpGet("ToNexusModsModUnlink")] - public async Task> ToNexusModsModUnlinkAsync([FromQuery] NexusModsUserToNexusModsModQuery query, [BindUserId] NexusModsUserId userId) + [HttpPatch("NexusModsModLinks")] + public async Task> UpdateNexusModsModLinkAsync([FromQuery, Required] NexusModsModId modId, [FromQuery] NexusModsUserId? userId, [FromQuery] NexusModsUserName? username, [BindTenant] TenantId tenant, CancellationToken ct) + { + var nexusModsUserId = await GetUserIdAsync(userId, username, ct); + if (nexusModsUserId == NexusModsUserId.None) + return ApiBadRequest("User not found!"); + + var currentUserId = HttpContext.GetUserId(); + if (currentUserId != nexusModsUserId && HttpContext.GetRole() != ApplicationRoles.Moderator && HttpContext.GetRole() != ApplicationRoles.Administrator) + return ApiBadRequest("Permission denied!"); + + if (HttpContext.GetAPIKey() is { } apiKey && apiKey != NexusModsApiKey.None) + return await UpdateNexusModsModLinkWithApiKeyAsync(apiKey, modId, nexusModsUserId, tenant, ct); + + // TODO: + return ApiBadRequest("Token auth not supported yet!"); + } + private async Task> UpdateNexusModsModLinkWithApiKeyAsync(NexusModsApiKey apiKey, NexusModsModId modId, NexusModsUserId userId, TenantId tenant, CancellationToken ct) { - await _dbContextWrite.NexusModsUserToNexusModsMods - .Where(x => x.NexusModsUser.NexusModsUserId == userId && x.NexusModsMod.NexusModsModId == query.NexusModsModId && x.LinkType == NexusModsUserToNexusModsModLinkType.ByOwner) - .ExecuteDeleteAsync(); + var gameDomain = tenant.ToGameDomain(); + + if (await _nexusModsAPIClient.GetModAsync(gameDomain, modId, apiKey, ct) is not { } modInfo) + return ApiBadRequest("Mod not found!"); + + if (userId != modInfo.User.Id) + return ApiBadRequest("User does not have access to the mod!"); + + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(); + + var nexusModsModToName = new NexusModsModToNameEntity + { + TenantId = tenant, + NexusModsModId = modId, + NexusModsMod = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modId), + Name = modInfo.Name, + }; + unitOfWrite.NexusModsModName.Upsert(nexusModsModToName); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); + return ApiResult("Updated successful!"); + } + + [HttpDelete("NexusModsModLinks")] + public async Task> RemoveNexusModsModLinkAsync([FromQuery, Required] NexusModsModId modId, [FromQuery] NexusModsUserId? userId, [FromQuery] NexusModsUserName? username, CancellationToken ct) + { + var nexusModsUserId = await GetUserIdAsync(userId, username, ct); + if (nexusModsUserId == NexusModsUserId.None) + return ApiBadRequest("User not found!"); + + var currentUserId = HttpContext.GetUserId(); + if (currentUserId != nexusModsUserId && HttpContext.GetRole() != ApplicationRoles.Moderator && HttpContext.GetRole() != ApplicationRoles.Administrator) + return ApiBadRequest("Permission denied!"); + + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(); + + unitOfWrite.NexusModsUserToNexusModsMods + .Remove(x => x.NexusModsUser.NexusModsUserId == nexusModsUserId && x.NexusModsMod.NexusModsModId == modId && x.LinkType == NexusModsUserToNexusModsModLinkType.ByOwner); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return ApiResult("Unlinked successful!"); } - [HttpGet("ToModuleManualLink")] + [HttpPost("ModuleManualLinks")] [ButrNexusModsAuthorization(Roles = $"{ApplicationRoles.Administrator},{ApplicationRoles.Moderator}")] - public async Task> ToModuleManualLinkAsync([FromQuery] NexusModsUserToModuleQuery query, [BindTenant] TenantId tenant) + public async Task> AddModuleManualLinkAsync([FromQuery, Required] ModuleId moduleId, [FromQuery] NexusModsUserId? userId, [FromQuery] NexusModsUserName? username, [BindTenant] TenantId tenant, CancellationToken ct) { - await using var _ = await _dbContextWrite.CreateSaveScopeAsync(); - var entityFactory = _dbContextWrite.GetEntityFactory(); + var nexusModsUserId = await GetUserIdAsync(userId, username, ct); + if (nexusModsUserId == NexusModsUserId.None) + return ApiBadRequest("User not found!"); + + var currentUserId = HttpContext.GetUserId(); + if (currentUserId != nexusModsUserId && HttpContext.GetRole() != ApplicationRoles.Moderator && HttpContext.GetRole() != ApplicationRoles.Administrator) + return ApiBadRequest("Permission denied!"); + + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(); var nexusModsUserToModule = new NexusModsUserToModuleEntity { TenantId = tenant, - NexusModsUser = entityFactory.GetOrCreateNexusModsUser(query.NexusModsUserId), - Module = entityFactory.GetOrCreateModule(query.ModuleId), + NexusModsUserId = nexusModsUserId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(nexusModsUserId), + ModuleId = moduleId, + Module = unitOfWrite.UpsertEntityFactory.GetOrCreateModule(moduleId), LinkType = NexusModsUserToModuleLinkType.ByStaff, }; - await _dbContextWrite.NexusModsUserToModules.UpsertOnSaveAsync(nexusModsUserToModule); + unitOfWrite.NexusModsUserToModules.Upsert(nexusModsUserToModule); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return ApiResult("Allowed successful!"); } - [HttpGet("ToModuleManualUnlink")] + [HttpDelete("ModuleManualLinks")] [ButrNexusModsAuthorization(Roles = $"{ApplicationRoles.Administrator},{ApplicationRoles.Moderator}")] - public async Task> ToModuleManualUnlinkAsync([FromQuery] NexusModsUserToModuleQuery query) + public async Task> RemoveModuleManualLinkAsync([FromQuery, Required] ModuleId moduleId, [FromQuery] NexusModsUserId? userId, [FromQuery] NexusModsUserName? username, CancellationToken ct) { - await _dbContextWrite.NexusModsUserToModules - .Where(x => x.NexusModsUser.NexusModsUserId == query.NexusModsUserId && x.Module.ModuleId == query.ModuleId && x.LinkType == NexusModsUserToModuleLinkType.ByStaff) - .ExecuteDeleteAsync(); + var nexusModsUserId = await GetUserIdAsync(userId, username, ct); + if (nexusModsUserId == NexusModsUserId.None) + return ApiBadRequest("User not found!"); + + var currentUserId = HttpContext.GetUserId(); + if (currentUserId != nexusModsUserId && HttpContext.GetRole() != ApplicationRoles.Moderator && HttpContext.GetRole() != ApplicationRoles.Administrator) + return ApiBadRequest("Permission denied!"); + + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(); + + unitOfWrite.NexusModsUserToModules + .Remove(x => x.NexusModsUser.NexusModsUserId == nexusModsUserId && x.Module.ModuleId == moduleId && x.LinkType == NexusModsUserToModuleLinkType.ByStaff); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return ApiResult("Disallowed successful!"); } - [HttpPost("ToModuleManualLinkPaginated")] + [HttpPost("ModuleManualLinks/Paginated")] [ButrNexusModsAuthorization(Roles = $"{ApplicationRoles.Administrator},{ApplicationRoles.Moderator}")] - public async Task?>> ToModuleManualLinkPaginatedAsync([FromBody] PaginatedQuery query, CancellationToken ct) + public async Task?>> GetModuleManualLinkPaginatedAsync([FromBody, Required] PaginatedQuery query, CancellationToken ct) { - var paginated = await _dbContextRead.NexusModsUserToModules - .Where(x => x.LinkType == NexusModsUserToModuleLinkType.ByStaff) - .GroupBy(x => new { x.NexusModsUser.NexusModsUserId }) - .Select(x => new { NexusModsUserId = x.Key.NexusModsUserId, ModuleIds = x.Select(y => y.Module.ModuleId).ToArray() }) - .PaginatedAsync(query, 20, new() { Property = nameof(NexusModsUserEntity.NexusModsUserId), Type = SortingType.Ascending }, ct); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); + + var paginated = await unitOfRead.NexusModsUserToModules.GetManuallyLinkedModuleIdsPaginatedAsync(query, NexusModsUserToModuleLinkType.ByStaff, ct); - return ApiPagingResult(paginated, items => items.Select(m => new NexusModsUserToModuleManualLinkModel(m.NexusModsUserId, m.ModuleIds.AsImmutableArray()))); + return ApiPagingResult(paginated); } - [HttpGet("ToNexusModsModManualLink")] - public async Task> ToNexusModsModManualLinkAsync([FromQuery] NexusModsUserToNexusModsModManualLinkQuery query, [BindApiKey] NexusModsApiKey apiKey, [BindUserId] NexusModsUserId userId, [BindTenant] TenantId tenant, CancellationToken ct) + [HttpPost("NexusModsModManualLinks")] + public async Task> AddNexusModsModManualLinkAsync([FromQuery, Required] NexusModsModId modId, [FromQuery] NexusModsUserId? userId, [FromQuery] NexusModsUserName? username, [BindTenant] TenantId tenant, CancellationToken ct) { - await using var _ = await _dbContextWrite.CreateSaveScopeAsync(); - var entityFactory = _dbContextWrite.GetEntityFactory(); + var nexusModsUserId = await GetUserIdAsync(userId, username, ct); + if (nexusModsUserId == NexusModsUserId.None) + return ApiBadRequest("User not found!"); + + var currentUserId = HttpContext.GetUserId(); + if (currentUserId != nexusModsUserId && HttpContext.GetRole() != ApplicationRoles.Moderator && HttpContext.GetRole() != ApplicationRoles.Administrator) + return ApiBadRequest("Permission denied!"); + + if (HttpContext.GetAPIKey() is { } apiKey && apiKey != NexusModsApiKey.None) + return await AddNexusModsModManualLinkWithApiKeyAsync(apiKey, modId, nexusModsUserId, tenant, ct); + // TODO: + return ApiBadRequest("Token auth not supported yet!"); + } + private async Task> AddNexusModsModManualLinkWithApiKeyAsync(NexusModsApiKey apiKey, NexusModsModId modId, NexusModsUserId userId, TenantId tenant, CancellationToken ct) + { var gameDomain = tenant.ToGameDomain(); - if (await _nexusModsAPIClient.GetModAsync(gameDomain, query.NexusModsModId, apiKey, ct) is not { } modInfo) + if (await _nexusModsAPIClient.GetModAsync(gameDomain, modId, apiKey, ct) is not { } modInfo) return ApiBadRequest("Mod not found!"); if (userId != modInfo.User.Id) return ApiBadRequest("User does not have access to the mod!"); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(); var nexusModsModToName = new NexusModsModToNameEntity { TenantId = tenant, - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(query.NexusModsModId), + NexusModsModId = modId, + NexusModsMod = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modId), Name = modInfo.Name, }; - await _dbContextWrite.NexusModsModName.UpsertOnSaveAsync(nexusModsModToName); + unitOfWrite.NexusModsModName.Upsert(nexusModsModToName); + var nexusModsUserToNexusModsMods = new NexusModsUserToNexusModsModEntity[] { new() { TenantId = tenant, - NexusModsUser = entityFactory.GetOrCreateNexusModsUser(userId), - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(query.NexusModsModId), + NexusModsUserId = userId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(userId), + NexusModsModId = modId, + NexusModsMod = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modId), LinkType = NexusModsUserToNexusModsModLinkType.ByAPIConfirmation, }, new() { TenantId = tenant, - NexusModsUser = entityFactory.GetOrCreateNexusModsUser(query.NexusModsUserId), - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(query.NexusModsModId), + NexusModsUserId = userId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(userId), + NexusModsModId = modId, + NexusModsMod = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modId), LinkType = NexusModsUserToNexusModsModLinkType.ByOwner, } }; - await _dbContextWrite.NexusModsUserToNexusModsMods.UpsertOnSaveAsync(nexusModsUserToNexusModsMods); + unitOfWrite.NexusModsUserToNexusModsMods.UpsertRange(nexusModsUserToNexusModsMods); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return ApiResult("Allowed successful!"); } - [HttpGet("ToNexusModsModManualUnlink")] - public async Task> ToNexusModsModManualUnlinkAsync([FromQuery] NexusModsUserToNexusModsModManualLinkQuery query) + [HttpDelete("NexusModsModManualLinks")] + public async Task> RemoveNexusModsModManualLinkAsync([FromQuery, Required] NexusModsModId modId, [FromQuery] NexusModsUserId? userId, [FromQuery] NexusModsUserName? username, CancellationToken ct) { - await _dbContextWrite.NexusModsUserToNexusModsMods - .Where(x => x.NexusModsUser.NexusModsUserId == query.NexusModsUserId && x.NexusModsMod.NexusModsModId == query.NexusModsModId && x.LinkType == NexusModsUserToNexusModsModLinkType.ByOwner) - .ExecuteDeleteAsync(); + var nexusModsUserId = await GetUserIdAsync(userId, username, ct); + if (nexusModsUserId == NexusModsUserId.None) + return ApiBadRequest("User not found!"); + + var currentUserId = HttpContext.GetUserId(); + if (currentUserId != nexusModsUserId && HttpContext.GetRole() != ApplicationRoles.Moderator && HttpContext.GetRole() != ApplicationRoles.Administrator) + return ApiBadRequest("Permission denied!"); + + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(); + + unitOfWrite.NexusModsUserToNexusModsMods + .Remove(x => x.NexusModsUser.NexusModsUserId == nexusModsUserId && x.NexusModsMod.NexusModsModId == modId && x.LinkType == NexusModsUserToNexusModsModLinkType.ByOwner); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return ApiResult("Disallowed successful!"); } - [HttpPost("ToNexusModsModManualLinkPaginated")] - public async Task?>> ToNexusModsModManualLinkPaginatedAsync([FromBody] PaginatedQuery query, [BindUserId] NexusModsUserId userId, CancellationToken ct) + [HttpPost("NexusModsModManualLinks/Paginated")] + public async Task?>> GetNexusModsModManualLinkPaginatedAsync([FromBody] PaginatedQuery query, CancellationToken ct) { - var paginated = await _dbContextRead.NexusModsUserToNexusModsMods - .Where(x => x.NexusModsUser.NexusModsUserId == userId && x.LinkType == NexusModsUserToNexusModsModLinkType.ByOwner) - .GroupBy(x => new { NexusModsModId = x.NexusModsMod.NexusModsModId }) - .Select(x => new { NexusModsModId = x.Key.NexusModsModId, NexusModsUserIds = x.Select(y => y.NexusModsUser.NexusModsUserId).ToArray() }) - .PaginatedAsync(query, 20, new() { Property = nameof(NexusModsModEntity.NexusModsModId), Type = SortingType.Ascending }, ct); + var userId = HttpContext.GetUserId(); + + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); + + var paginated = await unitOfRead.NexusModsUserToNexusModsMods.GetManuallyLinkedPaginatedAsync(userId, query, ct); - return ApiPagingResult(paginated, items => items.Select(m => new NexusModsUserToNexusModsModManualLinkModel(m.NexusModsModId, m.NexusModsUserIds.AsImmutableArray()))); + return ApiPagingResult(paginated); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/QuartzController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/QuartzController.cs index d2952aed..be3b53bd 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/QuartzController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/QuartzController.cs @@ -1,21 +1,20 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.API; using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Utils; using BUTR.Site.NexusMods.Server.Utils.BindingSources; using BUTR.Site.NexusMods.Server.Utils.Http.ApiResults; using BUTR.Site.NexusMods.Shared.Helpers; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Quartz; using System; -using System.Linq; +using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; @@ -25,41 +24,19 @@ namespace BUTR.Site.NexusMods.Server.Controllers; public sealed class QuartzController : ApiControllerBase { private readonly ILogger _logger; - private readonly IAppDbContextRead _dbContextRead; - private readonly IAppDbContextWrite _dbContextWrite; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; private readonly ISchedulerFactory _schedulerFactory; - public QuartzController(ILogger logger, IAppDbContextRead dbContextRead, IAppDbContextWrite dbContextWrite, ISchedulerFactory schedulerFactory) + public QuartzController(ILogger logger, IUnitOfWorkFactory unitOfWorkFactory, ISchedulerFactory schedulerFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _dbContextRead = dbContextRead ?? throw new ArgumentNullException(nameof(dbContextRead)); - _dbContextWrite = dbContextWrite ?? throw new ArgumentNullException(nameof(dbContextWrite)); - _schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory)); + _logger = logger; + _unitOfWorkFactory = unitOfWorkFactory; + _schedulerFactory = schedulerFactory; } - [HttpPost("HistoryPaginated")] - public async Task?>> HistoryPaginatedAsync([FromBody] PaginatedQuery query, CancellationToken ct) + [HttpPost("Triggers")] + public async Task> AddTriggerAsync([FromQuery, Required] string jobId, [BindUserId] NexusModsUserId userId, [BindUserName] NexusModsUserName userName, CancellationToken ct) { - var paginated = await _dbContextRead.QuartzExecutionLogs.Prepare() - .PaginatedAsync(query, 100, new() { Property = nameof(QuartzExecutionLogEntity.DateAddedUtc), Type = SortingType.Descending }, ct); - - return ApiPagingResult(paginated); - } - - [HttpGet("Delete")] - public async Task> DeleteAsync([FromQuery] long logId) - { - if (await _dbContextWrite.QuartzExecutionLogs.Where(x => x.LogId == logId).ExecuteDeleteAsync() > 0) - return ApiResult("Deleted successful!"); - - return ApiBadRequest("Failed to delete!"); - } - - [HttpGet("TriggerJob")] - public async Task> TriggerJobAsync(string jobId, [BindUserId] NexusModsUserId userId, CancellationToken ct) - { - var userName = HttpContext.GetName(); - var jobKey = JobKey.Create(jobId); var trigger = TriggerBuilder.Create() .WithIdentity($"User:{userId}:{userName}") @@ -71,4 +48,27 @@ public async Task> TriggerJobAsync(string jobId, [Bind var startTime = await scheduler.ScheduleJob(trigger, CancellationToken.None); return ApiResult(startTime); } + + [HttpPost("Jobs/Paginated")] + public async Task?>> JobsPaginatedAsync([FromBody, Required] PaginatedQuery query, CancellationToken ct) + { + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(TenantId.None); + + var paginated = await unitOfRead.QuartzExecutionLogs + .PaginatedAsync(query, 100, new() { Property = nameof(QuartzExecutionLogEntity.DateAddedUtc), Type = SortingType.Descending }, ct); + + return ApiPagingResult(paginated); + } + + [HttpDelete("Jobs")] + public async Task> RenoveJobAsync([FromQuery, Required] int logId) + { + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(TenantId.None); + + if (unitOfWrite.QuartzExecutionLogs.Remove(x => x.LogId == logId) <= 0) + return ApiBadRequest("Failed to delete!"); + + return ApiResult("Deleted successful!"); + + } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/RecreateStacktraceController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/RecreateStacktraceController.cs index 530d97d8..90be7e8d 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/RecreateStacktraceController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/RecreateStacktraceController.cs @@ -32,13 +32,14 @@ public sealed class RecreateStacktraceController : ApiControllerBase public RecreateStacktraceController(ILogger logger, ICrashReporterClient crashReporterClient, ISteamBinaryCache steamBinaryCache) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _crashReporterClient = crashReporterClient ?? throw new ArgumentNullException(nameof(crashReporterClient)); - _steamBinaryCache = steamBinaryCache ?? throw new ArgumentNullException(nameof(steamBinaryCache)); + _logger = logger; + _crashReporterClient = crashReporterClient; + _steamBinaryCache = steamBinaryCache; } - [HttpGet("Json/{id}")] - public async Task?>> JsonAsync(CrashReportFileId id, CancellationToken ct) + [HttpGet("Json")] + [Produces("application/json")] + public async Task?>> GetJsonAsync([FromQuery] CrashReportFileId id, CancellationToken ct) { if (!HttpContext.OwnsTenantGame()) return ApiResultError("Game is not owned!", StatusCodes.Status401Unauthorized); @@ -73,9 +74,9 @@ public RecreateStacktraceController(ILogger logger return ApiOk>(recreatedStacktraceWithMissing); } - [HttpGet("Html/{id}")] + [HttpGet("Html")] [Produces("text/plain")] - public async Task> HtmlAsync(CrashReportFileId id, CancellationToken ct) + public async Task> GetHtmlAsync([FromQuery] CrashReportFileId id, CancellationToken ct) { if (!HttpContext.OwnsTenantGame()) return Unauthorized(); diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/ReportsController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/ReportsController.cs index b43561bf..a9a4e880 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/ReportsController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/ReportsController.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using System; using System.Threading; using System.Threading.Tasks; @@ -22,14 +21,15 @@ public sealed class ReportsController : ApiControllerBase public ReportsController(ILogger logger, ICrashReporterClient crashReporterClient) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _crashReporterClient = crashReporterClient ?? throw new ArgumentNullException(nameof(crashReporterClient)); + _logger = logger; + _crashReporterClient = crashReporterClient; } - [HttpGet("Get/{id}.html")] + [HttpGet("{id}.html")] [Produces("text/html")] public async Task> GetAllAsync(CrashReportFileId id, CancellationToken ct) => Ok(await _crashReporterClient.GetCrashReportAsync(id, ct)); + // Just so we have ApiResult type in swagger.json [HttpGet("BlankRequest")] [Produces("text/plain")] public ApiResult BlankRequest() => ApiResultError("", StatusCodes.Status400BadRequest); diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/StatisticsController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/StatisticsController.cs index f133c924..a0d80ea9 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/StatisticsController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/StatisticsController.cs @@ -1,16 +1,14 @@ -using BUTR.Site.NexusMods.Server.Contexts; -using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Utils; using BUTR.Site.NexusMods.Server.Utils.Http.ApiResults; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -22,99 +20,58 @@ public sealed class StatisticsController : ApiControllerBase { public record TopExceptionsEntry(ExceptionTypeId Type, decimal Percentage); - public record VersionScore + private readonly ILogger _logger; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; + + public StatisticsController(ILogger logger, IUnitOfWorkFactory unitOfWorkFactory) { - public required ModuleVersion Version { get; init; } - public required double Score { get; init; } - public required double Value { get; init; } - public required int CountStable { get; init; } - public required int CountUnstable { get; init; } - public double Count => CountStable + CountUnstable; + _logger = logger; + _unitOfWorkFactory = unitOfWorkFactory; } - public record VersionStorage - { - public required ModuleVersion Version { get; init; } - public required VersionScore[] Scores { get; init; } - public double MeanScore => Scores.Length == 0 ? 0 : 1 - (Scores.Sum(x => x.Value) / (double) Scores.Sum(x => x.Count)); - }; - public record ModuleStorage - { - public required ModuleId ModuleId { get; init; } - public required VersionStorage[] Versions { get; init; } - }; - public record GameStorage + [HttpGet("TopExceptionsTypes")] + public async Task?>> GetTopExceptionsTypesAsync(CancellationToken ct) { - public required GameVersion GameVersion { get; init; } - public required ModuleStorage[] Modules { get; init; } - } + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); - private readonly ILogger _logger; - private readonly IAppDbContextRead _dbContextRead; + var types = await unitOfRead.StatisticsTopExceptionsTypes.GetAllAsync(null, null, ct); - public StatisticsController(ILogger logger, IAppDbContextRead dbContextRead) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _dbContextRead = dbContextRead ?? throw new ArgumentNullException(nameof(dbContextRead)); - } + var total = (decimal) types.Sum(x => x.ExceptionCount); - [HttpGet("AutocompleteGameVersion")] - public ApiResult?> AutocompleteGameVersion([FromQuery] GameVersion gameVersion) - { - return ApiResult(_dbContextRead.AutocompleteStartsWith(x => x.GameVersion, gameVersion)); + return ApiResult(types.Select(x => new TopExceptionsEntry(x.ExceptionType.ExceptionTypeId, ((decimal) x.ExceptionCount / total) * 100M))); } - [HttpGet("AutocompleteModuleId")] - public ApiResult?> AutocompleteModuleId([FromQuery] ModuleId moduleId) + [HttpGet("InvolvedModules")] + public async Task?>> GetInvolvedModulesAsync([FromQuery] GameVersion[]? gameVersions, [FromQuery] ModuleId[]? moduleIds, [FromQuery] ModuleVersion[]? moduleVersions) { - return ApiResult(_dbContextRead.AutocompleteStartsWith(x => x.Module.ModuleId, moduleId)); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); + + //if (gameVersions?.Length == 0 && modIds?.Length == 0 && modVersions?.Length == 0) + // return StatusCode(StatusCodes.Status403Forbidden, Array.Empty()); + + var data = await unitOfRead.StatisticsCrashScoreInvolveds.GetAllInvolvedModuleScoresForGameVersionAsync(gameVersions, moduleIds, moduleVersions, CancellationToken.None); + + return ApiResult(data); } - [HttpGet("TopExceptionsTypes")] - public async Task?>> TopExceptionsTypesAsync(CancellationToken ct) + + [HttpGet("Autocomplete/GameVersions")] + public async Task?>> GetAutocompleteGameVersionsAsync([FromQuery, Required] GameVersion gameVersion) { - var types = await _dbContextRead.StatisticsTopExceptionsTypes - .Include(x => x.ExceptionType) - .ToArrayAsync(ct); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); - var total = (decimal) types.Sum(x => x.ExceptionCount); + var gameVersions = await unitOfRead.Autocompletes.AutocompleteStartsWithAsync(x => x.GameVersion, gameVersion, CancellationToken.None); - return ApiResult(types.Select(x => new TopExceptionsEntry(x.ExceptionType.ExceptionTypeId, ((decimal) x.ExceptionCount / total) * 100M))); + return ApiResult(gameVersions); } - [HttpGet("Involved")] - public ApiResult?> Involved([FromQuery] GameVersion[]? gameVersions, [FromQuery] ModuleId[]? moduleIds, [FromQuery] ModuleVersion[]? moduleVersions) + [HttpGet("Autocomplete/ModuleIds")] + public async Task?>> GetAutocompleteModuleIdsAsync([FromQuery, Required] ModuleId moduleId) { - //if (gameVersions?.Length == 0 && modIds?.Length == 0 && modVersions?.Length == 0) - // return StatusCode(StatusCodes.Status403Forbidden, Array.Empty()); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); - var data = _dbContextRead.StatisticsCrashScoreInvolveds - .Include(x => x.Module) - .WhereIf(gameVersions != null && gameVersions.Length != 0, x => gameVersions!.Contains(x.GameVersion)) - .WhereIf(moduleIds != null && moduleIds.Length != 0, x => moduleIds!.Contains(x.Module.ModuleId)) - .WhereIf(moduleVersions != null && moduleVersions.Length != 0, x => moduleVersions!.Contains(x.ModuleVersion)) - .GroupBy(x => new { x.GameVersion }) - .Select(x => new GameStorage - { - GameVersion = x.Key.GameVersion, - Modules = x.GroupBy(y => new { y.Module.ModuleId }).Select(y => new ModuleStorage - { - ModuleId = y.Key.ModuleId, - Versions = y.GroupBy(z => new { z.ModuleVersion }).Select(z => new VersionStorage - { - Version = z.Key.ModuleVersion, - Scores = z.Select(q => new VersionScore - { - Version = z.Key.ModuleVersion, - Score = 1 - q.Score, - Value = q.RawValue, - CountStable = q.NotInvolvedCount, - CountUnstable = q.InvolvedCount, - }).ToArray(), - }).ToArray(), - }).ToArray(), - }); + var moduleIds = await unitOfRead.Autocompletes.AutocompleteStartsWithAsync(x => x.Module.ModuleId, moduleId, CancellationToken.None); - return ApiResult(data); + return ApiResult(moduleIds); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/SteamController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/SteamController.cs index c20b1682..ce07f31a 100644 --- a/src/BUTR.Site.NexusMods.Server/Controllers/SteamController.cs +++ b/src/BUTR.Site.NexusMods.Server/Controllers/SteamController.cs @@ -1,9 +1,7 @@ using BUTR.Site.NexusMods.Server.Extensions; -using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Options; using BUTR.Site.NexusMods.Server.Services; using BUTR.Site.NexusMods.Server.Utils; -using BUTR.Site.NexusMods.Server.Utils.BindingSources; using BUTR.Site.NexusMods.Server.Utils.Http.ApiResults; using BUTR.Site.NexusMods.Shared.Helpers; @@ -13,6 +11,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; @@ -31,32 +30,17 @@ public sealed record SteamOpenIdUrlModel(string Url); public SteamController(ISteamStorage steamStorage, IOptions options, ISteamCommunityClient steamCommunityClient, ISteamAPIClient steamAPIClient) { - _steamStorage = steamStorage ?? throw new ArgumentNullException(nameof(steamStorage)); - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); - _steamCommunityClient = steamCommunityClient ?? throw new ArgumentNullException(nameof(steamCommunityClient)); - _steamAPIClient = steamAPIClient ?? throw new ArgumentNullException(nameof(steamAPIClient)); + _steamStorage = steamStorage; + _options = options.Value; + _steamCommunityClient = steamCommunityClient; + _steamAPIClient = steamAPIClient; } - [HttpGet("GetOpenIdUrl")] - public ApiResult GetOpenIdUrl() + [HttpPost] + public async Task> AddLinkAsync([FromQuery, Required] Dictionary queries, CancellationToken ct) { - var query = QueryString.Create(new Dictionary - { - ["openid.ns"] = "http://specs.openid.net/auth/2.0", - ["openid.mode"] = "checkid_setup", - ["openid.return_to"] = _options.RedirectUri, - ["openid.realm"] = _options.Realm, - ["openid.identity"] = "http://specs.openid.net/auth/2.0/identifier_select", - ["openid.claimed_id"] = "http://specs.openid.net/auth/2.0/identifier_select", - }); - var steamLoginUrl = new UriBuilder("https://steamcommunity.com/openid/login") { Query = query.ToUriComponent() }; - - return ApiResult(new SteamOpenIdUrlModel(steamLoginUrl.ToString())); - } + var userId = HttpContext.GetUserId(); - [HttpGet("Link")] - public async Task> LinkAsync([FromQuery] Dictionary queries, [BindUserId] NexusModsUserId userId, CancellationToken ct) - { var isValid = await _steamCommunityClient.ConfirmIdentityAsync(queries, ct); if (!isValid) return ApiBadRequest("Failed to link!"); @@ -75,9 +59,11 @@ public SteamController(ISteamStorage steamStorage, IOptions opt return ApiResult("Linked successful!"); } - [HttpPost("Unlink")] - public async Task> UnlinkAsync([BindUserId] NexusModsUserId userId) + [HttpDelete] + public async Task> RemoveLinkAsync() { + var userId = HttpContext.GetUserId(); + var tokens = HttpContext.GetSteamTokens(); if (tokens?.Data is null) @@ -89,8 +75,25 @@ public SteamController(ISteamStorage steamStorage, IOptions opt return ApiResult("Unlinked successful!"); } - [HttpPost("GetUserInfo")] - public async Task> GetUserInfoByAccessTokenAsync(CancellationToken ct) + [HttpGet("OpenIdUrl")] + public ApiResult GetOpenIdUrl() + { + var query = QueryString.Create(new Dictionary + { + ["openid.ns"] = "http://specs.openid.net/auth/2.0", + ["openid.mode"] = "checkid_setup", + ["openid.return_to"] = _options.RedirectUri, + ["openid.realm"] = _options.Realm, + ["openid.identity"] = "http://specs.openid.net/auth/2.0/identifier_select", + ["openid.claimed_id"] = "http://specs.openid.net/auth/2.0/identifier_select", + }); + var steamLoginUrl = new UriBuilder("https://steamcommunity.com/openid/login") { Query = query.ToUriComponent() }; + + return ApiResult(new SteamOpenIdUrlModel(steamLoginUrl.ToString())); + } + + [HttpGet("UserInfo")] + public async Task> GetUserInfoAsync(CancellationToken ct) { var tokens = HttpContext.GetSteamTokens(); diff --git a/src/BUTR.Site.NexusMods.Server/Extensions/AppDbContextExtensions.cs b/src/BUTR.Site.NexusMods.Server/Extensions/AppDbContextExtensions.cs deleted file mode 100644 index 8e827496..00000000 --- a/src/BUTR.Site.NexusMods.Server/Extensions/AppDbContextExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -using BUTR.Site.NexusMods.Server.Contexts; -using BUTR.Site.NexusMods.Server.Jobs; - -using Microsoft.EntityFrameworkCore; - -using System; -using System.Linq; -using System.Linq.Expressions; - -namespace BUTR.Site.NexusMods.Server.Extensions; - -/// -/// Provides extension methods for objects. -/// -public static class AppDbContextExtensions -{ - /// - /// Returns an IQueryable of strings where the specified property of the entity starts with the provided value. - /// - /// The type of the entity. - /// The type of the property. - /// The database context to query. - /// The property of the entity to match. - /// The value to match the start of the property with. - /// An IQueryable of strings where the specified property of the entity starts with the provided value. - - public static IQueryable AutocompleteStartsWith(this IAppDbContextRead dbContext, Expression> property, TParameter value) - { - var key = AutocompleteProcessorProcessorJob.GenerateName(property); - return dbContext.Autocompletes - .Where(x => x.Type == key) - .Where(x => EF.Functions.ILike(x.Value, $"%{value}%")) - .OrderBy(x => x.Value) - .Select(x => x.Value); - } - - /// - /// Returns an IQueryable of strings where the specified property of the entity starts with the provided subValue and the property value matches the provided propertyValue. - /// - /// The type of the entity. - /// The database context to query. - /// The property of the entity to match. - /// The value of the property to match. - /// The sub value to match the start of the property with. - /// An IQueryable of strings where the specified property of the entity starts with the provided subValue and the property value matches the provided propertyValue. - public static IQueryable AutocompleteGroupStartsWith(this IAppDbContextRead dbContext, Expression> property, string propertyValue, string subValue) - { - var key = $"{AutocompleteProcessorProcessorJob.GenerateName(property)}.{propertyValue}"; - return dbContext.Autocompletes - .Where(x => x.Type == key) - .Where(x => EF.Functions.ILike(x.Value, $"%{subValue}%")) - .OrderBy(x => x.Value) - .Select(x => x.Value); - } -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Extensions/DbContextExtensions.cs b/src/BUTR.Site.NexusMods.Server/Extensions/DbContextExtensions.cs new file mode 100644 index 00000000..2aaa4599 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Extensions/DbContextExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; + +namespace BUTR.Site.NexusMods.Server.Extensions; + +public static class DbContextExtensions +{ + public static void UpdateProperties(this DbContext context, object target, object source) + { + var targetEntry = context.Entry(target); + foreach (var targetPropertyEntry in targetEntry.Properties) + { + var targetProperty = targetPropertyEntry.Metadata; + + // Skip shadow and key properties + if (targetProperty.IsShadowProperty() || (targetEntry.IsKeySet && targetProperty.IsKey())) continue; + targetPropertyEntry.CurrentValue = targetProperty.GetGetter().GetClrValue(source); + } + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Extensions/DbContextOptionsBuilderExtensions.cs b/src/BUTR.Site.NexusMods.Server/Extensions/DbContextOptionsBuilderExtensions.cs deleted file mode 100644 index 78136f55..00000000 --- a/src/BUTR.Site.NexusMods.Server/Extensions/DbContextOptionsBuilderExtensions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using BUTR.Site.NexusMods.Server.Utils.Npgsql; - -using Microsoft.EntityFrameworkCore; - -namespace BUTR.Site.NexusMods.Server.Extensions; - -public static class DbContextOptionsBuilderExtensions -{ - public static DbContextOptionsBuilder AddPrepareInterceptor(this DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.AddInterceptors(new PrepareCommandInterceptor()); -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Extensions/DbSetExtensions.cs b/src/BUTR.Site.NexusMods.Server/Extensions/DbSetExtensions.cs deleted file mode 100644 index 8b2393da..00000000 --- a/src/BUTR.Site.NexusMods.Server/Extensions/DbSetExtensions.cs +++ /dev/null @@ -1,73 +0,0 @@ -using BUTR.Site.NexusMods.Server.Contexts; -using BUTR.Site.NexusMods.Server.Models.Database; - -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace BUTR.Site.NexusMods.Server.Extensions; - -public static class DbSetExtensions -{ - public static async Task UpsertOnSaveAsync(this DbSet dbSet, IEnumerable entities) where TEntity : class, IEntity - { - if (dbSet.GetService().Context is IAppDbContextWrite dbContext) - await dbContext.BulkUpsertAsync(dbSet, entities); - } - public static async Task UpsertOnSaveAsync(this DbSet dbSet, IAsyncEnumerable entities) where TEntity : class, IEntity - { - if (dbSet.GetService().Context is IAppDbContextWrite dbContext) - await dbContext.BulkUpsertAsync(dbSet, entities); - } - public static async Task UpsertOnSaveAsync(this DbSet dbSet, params TEntity[] entities) where TEntity : class, IEntity - { - if (dbSet.GetService().Context is IAppDbContextWrite dbContext) - await dbContext.BulkUpsertAsync(dbSet, entities); - } - - public static async Task SynchronizeOnSaveAsync(this DbSet dbSet, IEnumerable entities) where TEntity : class, IEntity - { - if (dbSet.GetService().Context is IAppDbContextWrite dbContext) - await dbContext.BulkSynchronizeAsync(dbSet, entities); - } - public static async Task SynchronizeOnSaveAsync(this DbSet dbSet, IAsyncEnumerable entities) where TEntity : class, IEntity - { - if (dbSet.GetService().Context is IAppDbContextWrite dbContext) - await dbContext.BulkSynchronizeAsync(dbSet, entities); - } - public static async Task SynchronizeOnSaveAsync(this DbSet dbSet, params TEntity[] entities) where TEntity : class, IEntity - { - if (dbSet.GetService().Context is IAppDbContextWrite dbContext) - await dbContext.BulkSynchronizeAsync(dbSet, entities); - } - - - public static async Task UpsertAsync(this DbSet dbSet, IEnumerable entities, bool @unsafe = false) where TEntity : class, IEntity - { - await dbSet.BulkMergeAsync(entities, o => { o.UnsafeMode = @unsafe; o.UseInternalTransaction = true; }); - } - public static async Task UpsertAsync(this DbSet dbSet, IAsyncEnumerable entities, bool @unsafe = false) where TEntity : class, IEntity - { - await dbSet.BulkMergeAsync(await entities.ToArrayAsync(), o => { o.UnsafeMode = @unsafe; o.UseInternalTransaction = true; }); - } - public static async Task UpsertAsync(this DbSet dbSet, bool @unsafe = false, params TEntity[] entities) where TEntity : class, IEntity - { - await dbSet.BulkMergeAsync(entities, o => { o.UnsafeMode = @unsafe; o.UseInternalTransaction = true; }); - } - - public static async Task SynchronizeAsync(this DbSet dbSet, IEnumerable entities) where TEntity : class, IEntity - { - await dbSet.BulkSynchronizeAsync(entities, o => { o.UseInternalTransaction = true; }); - } - public static async Task SynchronizeAsync(this DbSet dbSet, IAsyncEnumerable entities) where TEntity : class, IEntity - { - await dbSet.BulkSynchronizeAsync(await entities.ToArrayAsync(), o => { o.UseInternalTransaction = true; }); - } - public static async Task SynchronizeAsync(this DbSet dbSet, params TEntity[] entities) where TEntity : class, IEntity - { - await dbSet.BulkSynchronizeAsync(entities, o => { o.UseInternalTransaction = true; }); - } -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Extensions/HttpContextExtensions.cs b/src/BUTR.Site.NexusMods.Server/Extensions/HttpContextExtensions.cs index 50601ab8..9c5b34b3 100644 --- a/src/BUTR.Site.NexusMods.Server/Extensions/HttpContextExtensions.cs +++ b/src/BUTR.Site.NexusMods.Server/Extensions/HttpContextExtensions.cs @@ -42,6 +42,31 @@ public static ProfileModel GetProfile(this HttpContext context, NexusModsValidat }).ToImmutableArray(), }; } + public static ProfileModel GetProfile(this HttpContext context, Services.NexusModsUserInfo userInfo, ApplicationRole role, Dictionary metadata) + { + var jsonSerializerOptions = context.RequestServices.GetRequiredService>().Value; + + return new ProfileModel + { + NexusModsUserId = NexusModsUserId.From(int.Parse(userInfo.UserId)), + Name = userInfo.Name, + Email = NexusModsUserEMail.Empty, + ProfileUrl = "validate.ProfileUrl", + IsPremium = userInfo.MembershipRoles.Contains("supporter"), + IsSupporter = userInfo.MembershipRoles.Contains("premium"), + Role = role, + GitHubUserId = GetGitHubId(metadata, jsonSerializerOptions), + DiscordUserId = GetDiscordId(metadata, jsonSerializerOptions), + GOGUserId = GetGOGId(metadata, jsonSerializerOptions), + SteamUserId = GetSteamId(metadata, jsonSerializerOptions), + HasTenantGame = context.OwnsTenantGame(context.GetTenant()), + AvailableTenants = TenantId.Values.Select(x => new ProfileTenantModel + { + TenantId = x, + Name = x.ToName(), + }).ToImmutableArray(), + }; + } public static ProfileModel GetProfile(this HttpContext context) { @@ -112,6 +137,12 @@ public static NexusModsApiKey GetAPIKey(this HttpContext context) return NexusModsApiKey.From(apiKey); return NexusModsApiKey.None; } + public static NexusModsOAuthTokens? GetTokens(this HttpContext context) + { + if (context.User.FindFirst(ButrNexusModsClaimTypes.AccessToken)?.Value is { } accessToken && context.User.FindFirst(ButrNexusModsClaimTypes.RefreshToken)?.Value is { } refreshToken) + return new NexusModsOAuthTokens(accessToken, refreshToken); + return null; + } public static ApplicationRole GetRole(this HttpContext context) { diff --git a/src/BUTR.Site.NexusMods.Server/Extensions/IServiceScopeExtensions.cs b/src/BUTR.Site.NexusMods.Server/Extensions/IServiceScopeExtensions.cs new file mode 100644 index 00000000..64266964 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Extensions/IServiceScopeExtensions.cs @@ -0,0 +1,16 @@ +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models; + +using Microsoft.Extensions.DependencyInjection; + +namespace BUTR.Site.NexusMods.Server.Extensions; + +public static class IServiceScopeExtensions +{ + public static TServiceScope WithTenant(this TServiceScope serviceScope, TenantId tenant) where TServiceScope : IServiceScope + { + var tenantContextAccessor = serviceScope.ServiceProvider.GetRequiredService(); + tenantContextAccessor.Current = tenant; + return serviceScope; + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Extensions/QueryableExtensions.cs b/src/BUTR.Site.NexusMods.Server/Extensions/QueryableExtensions.cs index 0d7c5419..b5c15efd 100644 --- a/src/BUTR.Site.NexusMods.Server/Extensions/QueryableExtensions.cs +++ b/src/BUTR.Site.NexusMods.Server/Extensions/QueryableExtensions.cs @@ -3,7 +3,7 @@ using BUTR.Site.NexusMods.Server.Models.API; using BUTR.Site.NexusMods.Server.Models.Database; using BUTR.Site.NexusMods.Server.Utils; -using BUTR.Site.NexusMods.Server.Utils.Npgsql; +using BUTR.Site.NexusMods.Server.ValueObjects.Utils; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; @@ -29,12 +29,15 @@ public static class QueryableExtensions public static IQueryable WhereIf(this IQueryable source, bool condition, Expression> predicate) => condition ? source.Where(predicate) : source; + public static IQueryable OrderByIf(this IQueryable source, bool condition, Func, IOrderedQueryable> orderBy) => + condition ? orderBy(source) : source; + public static async IAsyncEnumerable> BatchedAsync(this IQueryable query, int batchSize = 3000) { var processed = 0; - Task> FetchNext() => query.Skip(processed).Take(batchSize).ToImmutableArrayAsync(); - var fetch = FetchNext(); + Task> FetchNextAsync() => query.Skip(processed).Take(batchSize).ToImmutableArrayAsync(); + var fetch = FetchNextAsync(); while (true) { @@ -44,13 +47,14 @@ public static async IAsyncEnumerable> BatchedAsync(this IQu processed += toProcess.Length; // Start pre-fetching the next batch if there's still available - fetch = toProcess.Length == batchSize ? FetchNext() : Task.FromResult(ImmutableArray.Empty); + fetch = toProcess.Length == batchSize ? FetchNextAsync() : Task.FromResult(ImmutableArray.Empty); yield return toProcess; } } - public static Task> PaginatedAsync(this IQueryable queryable, PaginatedQuery query, uint maxPageSize = 20, Sorting? defaultSorting = default, CancellationToken ct = default) where TEntity : class + public static Task> PaginatedAsync(this IQueryable queryable, PaginatedQuery query, uint maxPageSize = 20, Sorting? defaultSorting = default, CancellationToken ct = default) + where TEntity : class { var page = query.Page; var pageSize = Math.Max(Math.Min(query.PageSize, maxPageSize), 5); @@ -66,7 +70,8 @@ public static Task> PaginatedAsync(this IQueryable> PaginatedAsync(this IQueryable queryable, uint page, uint pageSize, CancellationToken ct = default) where TEntity : class + public static async Task> PaginatedAsync(this IQueryable queryable, uint page, uint pageSize, CancellationToken ct = default) + where TEntity : class { var startTime = Stopwatch.GetTimestamp(); var count = await queryable.CountAsync(ct); @@ -108,7 +113,7 @@ public static Task> ToImmutableArrayAsync(this private static bool TryConvertValue(Type type, string rawValue, [NotNullWhen((true))] out object? value) { - type = QueryableHelper.ConvertValueObject(type); + type = VogenUtils.ConvertValueObject(type); if (type.IsEnum) return Enum.TryParse(type, rawValue, out value); @@ -294,9 +299,4 @@ public static IQueryable WithSort(this IQueryable que return ordered ?? queryable; } - - public static IQueryable Prepare(this IQueryable query) - { - return query.TagWith(PrepareCommandInterceptor.Tag); - } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Jobs/AutocompleteProcessorProcessorJob.cs b/src/BUTR.Site.NexusMods.Server/Jobs/AutocompleteProcessorProcessorJob.cs index 0ee4d52b..54273ad1 100644 --- a/src/BUTR.Site.NexusMods.Server/Jobs/AutocompleteProcessorProcessorJob.cs +++ b/src/BUTR.Site.NexusMods.Server/Jobs/AutocompleteProcessorProcessorJob.cs @@ -1,16 +1,13 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; -using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Quartz; using System; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Threading; @@ -21,29 +18,29 @@ namespace BUTR.Site.NexusMods.Server.Jobs; [DisallowConcurrentExecution] public sealed class AutocompleteProcessorProcessorJob : IJob { - private sealed record AutocompleteEntry(string Name, Func> Query); - /* + private sealed record AutocompleteEntry(string Name, Func> Query); + private sealed record GroupingEntry { public required string Key { get; init; } public required string[] Values { get; init; } } - private sealed record AutocompleteGroupingEntry(string Name, Func> Query); - */ + private sealed record AutocompleteGroupingEntry(string Name, Func> Query); private static readonly AutocompleteEntry[] ToAutocomplete = - { - new(GenerateName(x => x.GameVersion), x => x.CrashReports.Select(y => y.GameVersion.Value)), - new(GenerateName(x => x.Module.ModuleId), x => x.CrashReportModuleInfos.Select(y => y.Module.ModuleId.Value)), - new(GenerateName(x => x.NexusModsUser.Name!.Name), x => x.NexusModsArticles.Include(y => y.NexusModsUser).ThenInclude(y => y.Name).Select(y => y.NexusModsUser).Select(y => y.Name!).Select(y => y.Name.Value)), - }; - - //private static readonly AutocompleteGroupingEntry[] ToAutocompleteGrouping = - //{ - // new(GenerateName(x => x.Module.ModuleId), - // x => x.CrashReportModuleInfos.GroupBy(y => y.Module.ModuleId).Select(y => new GroupingEntry { Key = y.Key, Values = y.Select(z => z.Version).Distinct().ToArray() })), - //}; + [ + new(GenerateName(x => x.GameVersion), x => x.CrashReports.GetAllGameVersions().Select(y => y.Value)), + new(GenerateName(x => x.Module.ModuleId), x => x.CrashReportModuleInfos.GetAllModuleIds().Select(y => y.Value)), + new(GenerateName(x => x.NexusModsUser.Name!.Name), x => x.NexusModsArticles.GetAllUserNames().Select(y => y.Value)) + ]; + + private static readonly AutocompleteGroupingEntry[] ToAutocompleteGrouping = + [ + new(GenerateName(x => x.Module.ModuleId), + x => x.CrashReportModuleInfos.GroupBy(y => y.Module.ModuleId).Select(y => new GroupingEntry { Key = y.Key, Values = y.Select(z => z.Version).Distinct().ToArray() })), + ]; + */ public static string GenerateName(Expression> property) { @@ -56,8 +53,8 @@ public static string GenerateName(Expression logger, IServiceScopeFactory serviceScopeFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + _logger = logger; + _serviceScopeFactory = serviceScopeFactory; } public async Task Execute(IJobExecutionContext context) @@ -68,62 +65,23 @@ public async Task Execute(IJobExecutionContext context) foreach (var tenant in TenantId.Values) { - await using var scope = _serviceScopeFactory.CreateAsyncScope(); - - var tenantContextAccessor = scope.ServiceProvider.GetRequiredService(); - tenantContextAccessor.Current = tenant; - - await HandleTenantAsync(tenant, scope.ServiceProvider, ct); + await using var scope = _serviceScopeFactory.CreateAsyncScope().WithTenant(tenant); + await HandleTenantAsync(scope, ct); } context.Result = "Updated Autocomplete Data"; context.SetIsSuccess(true); } - private static async Task HandleTenantAsync(TenantId tenant, IServiceProvider serviceProvider, CancellationToken ct) + private async Task HandleTenantAsync(AsyncServiceScope scope, CancellationToken ct) { - var dbContextRead = serviceProvider.GetRequiredService(); - var dbContextWrite = serviceProvider.GetRequiredService(); - - foreach (var autocompleteEntry in ToAutocomplete) - { - if (ct.IsCancellationRequested) return; + var unitOfWorkFactory = scope.ServiceProvider.GetRequiredService(); + await using var unitOfWrite = unitOfWorkFactory.CreateUnitOfWrite(); - var key = autocompleteEntry.Name; + await unitOfWrite.NexusModsArticles.GenerateAutoCompleteForAuthorNameAsync(ct); + await unitOfWrite.CrashReports.GenerateAutoCompleteForGameVersionsAsync(ct); + await unitOfWrite.CrashReportModuleInfos.GenerateAutoCompleteForModuleIdsAsync(ct); - await dbContextWrite.Autocompletes.Where(x => x.Type == key).ExecuteDeleteAsync(ct); - await dbContextWrite.Autocompletes.UpsertAsync(autocompleteEntry.Query(dbContextRead).Distinct().Select(x => new AutocompleteEntity - { - TenantId = tenant, - Type = key, - Value = x, - }), true); - } - - /* - foreach (var autocompleteGroupingEntry in ToAutocompleteGrouping) - { - foreach (var chunk in autocompleteGroupingEntry.Query(dbContextRead).AsEnumerable().Chunk(50)) - { - var keys = chunk.Select(x => $"{autocompleteGroupingEntry.Name}.{x.Key}").ToArray(); - foreach (var groupingEntry in chunk) - { - if (ct.IsCancellationRequested) return; - - var key = $"{autocompleteGroupingEntry.Name}.{groupingEntry.Key}"; - await dbContextWrite.Autocompletes.AddRangeAsync(groupingEntry.Values.Select(x => new AutocompleteEntity - { - TenantId = tenant, - AutocompleteId = 0, - Type = key, - Value = x, - }), ct); - } - - await dbContextWrite.Autocompletes.Where(x => keys.Contains(x.Type)).ExecuteDeleteAsync(ct); - await dbContextWrite.SaveAsync(ct); - } - } - */ + await unitOfWrite.SaveChangesAsync(ct); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Jobs/CrashReportAnalyzerProcessorJob.cs b/src/BUTR.Site.NexusMods.Server/Jobs/CrashReportAnalyzerProcessorJob.cs index bbe6e0bc..ce6a2c33 100644 --- a/src/BUTR.Site.NexusMods.Server/Jobs/CrashReportAnalyzerProcessorJob.cs +++ b/src/BUTR.Site.NexusMods.Server/Jobs/CrashReportAnalyzerProcessorJob.cs @@ -1,9 +1,8 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -24,8 +23,8 @@ public sealed class CrashReportAnalyzerProcessorJob : IJob public CrashReportAnalyzerProcessorJob(ILogger logger, IServiceScopeFactory serviceScopeFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + _logger = logger; + _serviceScopeFactory = serviceScopeFactory; } public async Task Execute(IJobExecutionContext context) @@ -36,91 +35,38 @@ public async Task Execute(IJobExecutionContext context) foreach (var tenant in TenantId.Values) { - await using var scope = _serviceScopeFactory.CreateAsyncScope(); - - var tenantContextAccessor = scope.ServiceProvider.GetRequiredService(); - tenantContextAccessor.Current = tenant; - - try - { - await HandleTenantAsync(tenant, scope.ServiceProvider, ct); - - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } + await using var scope = _serviceScopeFactory.CreateAsyncScope().WithTenant(tenant); + await HandleTenantAsync(scope, tenant, ct); } context.Result = "Updated Crash Report Statistics Data"; context.SetIsSuccess(true); } - private static async Task HandleTenantAsync(TenantId tenant, IServiceProvider serviceProvider, CancellationToken ct) + private async Task HandleTenantAsync(AsyncServiceScope scope, TenantId tenant, CancellationToken ct) { - var dbContextRead = serviceProvider.GetRequiredService(); - var dbContextWrite = serviceProvider.GetRequiredService(); - var entityFactory = dbContextWrite.GetEntityFactory(); - await using var _ = await dbContextWrite.CreateSaveScopeAsync(); - - var allModVersionsQuery = dbContextRead.CrashReportModuleInfos - .GroupBy(x => new { x.Module.ModuleId, x.Version }) - .Select(x => new { x.Key.ModuleId, x.Key.Version }) - .Distinct(); - - var modCountsQuery = dbContextRead.CrashReportModuleInfos - .Include(x => x.ToCrashReport!) - .GroupBy(x => new { x.ToCrashReport!.GameVersion, x.Module.ModuleId, x.Version }) - .Select(x => new { x.Key.GameVersion, x.Key.ModuleId, x.Key.Version, Count = x.Count() }) - .Distinct(); - - var involvedModCountsQuery = dbContextRead.CrashReportModuleInfos - .Include(x => x.ToCrashReport!) - .Where(x => x.IsInvolved) - .GroupBy(x => new { x.ToCrashReport!.GameVersion, x.Module.ModuleId, x.Version }) - .Select(x => new { x.Key.GameVersion, x.Key.ModuleId, x.Key.Version, Count = x.Count() }) - .Distinct(); - - var notInvolvedModCountsQuery = dbContextRead.CrashReportModuleInfos - .Include(x => x.ToCrashReport!) - .Where(x => !x.IsInvolved) - .GroupBy(x => new { x.ToCrashReport!.GameVersion, x.Module.ModuleId, x.Version }) - .Select(x => new { x.Key.GameVersion, x.Key.ModuleId, x.Key.Version, Count = x.Count() }) - .Distinct(); + var unitOfWorkFactory = scope.ServiceProvider.GetRequiredService(); + await using var unitOfRead = unitOfWorkFactory.CreateUnitOfRead(); + await using var unitOfWrite = unitOfWorkFactory.CreateUnitOfWrite(); - var query = - from allModVersios in allModVersionsQuery - join modCounts in modCountsQuery on new { allModVersios.ModuleId, allModVersios.Version } equals new { modCounts.ModuleId, modCounts.Version } - join involvedModCounts in involvedModCountsQuery on new { allModVersios.ModuleId, allModVersios.Version, modCounts.GameVersion } equals new { involvedModCounts.ModuleId, involvedModCounts.Version, involvedModCounts.GameVersion } - join notInvolvedModCounts in notInvolvedModCountsQuery on new { allModVersios.ModuleId, allModVersios.Version, modCounts.GameVersion } equals new { notInvolvedModCounts.ModuleId, notInvolvedModCounts.Version, notInvolvedModCounts.GameVersion } - select new - { - GameVersion = modCounts.GameVersion, - ModuleId = allModVersios.ModuleId, - ModuleVersion = allModVersios.Version, - InvolvedCount = involvedModCounts.Count, - NotInvolvedCount = notInvolvedModCounts.Count, - TotalCount = modCounts.Count, - Value = involvedModCounts.Count, - CrashScore = (double) involvedModCounts.Count / (double) modCounts.Count - }; + var statisticsData = await unitOfRead.CrashReportModuleInfos.GetAllStatisticsAsync(ct); - var statisticsCrashScoreInvolved = await query.AsAsyncEnumerable().Select(x => new StatisticsCrashScoreInvolvedEntity + unitOfWrite.StatisticsCrashScoreInvolveds.Remove(x => true); + unitOfWrite.StatisticsCrashScoreInvolveds.UpsertRange(statisticsData.Select(x => new StatisticsCrashScoreInvolvedEntity { TenantId = tenant, StatisticsCrashScoreInvolvedId = Guid.NewGuid(), GameVersion = x.GameVersion, - Module = entityFactory.GetOrCreateModule(x.ModuleId), + ModuleId = x.ModuleId, + Module = unitOfWrite.UpsertEntityFactory.GetOrCreateModule(x.ModuleId), ModuleVersion = x.ModuleVersion, InvolvedCount = x.InvolvedCount, NotInvolvedCount = x.NotInvolvedCount, TotalCount = x.TotalCount, RawValue = x.Value, Score = x.CrashScore, - }).ToArrayAsync(ct); + }).ToList()); - await dbContextWrite.StatisticsCrashScoreInvolveds.SynchronizeOnSaveAsync(statisticsCrashScoreInvolved); - // Disposing the DBContext will save the data + await unitOfWrite.SaveChangesAsync(ct); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Jobs/CrashReportProcessorJob.cs b/src/BUTR.Site.NexusMods.Server/Jobs/CrashReportProcessorJob.cs index e064b4a2..4f7e41c7 100644 --- a/src/BUTR.Site.NexusMods.Server/Jobs/CrashReportProcessorJob.cs +++ b/src/BUTR.Site.NexusMods.Server/Jobs/CrashReportProcessorJob.cs @@ -1,4 +1,3 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Services; @@ -19,9 +18,43 @@ namespace BUTR.Site.NexusMods.Server.Jobs; public sealed class CrashReportProcessorJob : IJob { private readonly ILogger _logger; + private readonly ICrashReporterClient _crashReporterClient; private readonly IServiceScopeFactory _serviceScopeFactory; - public CrashReportProcessorJob(ILogger logger, IServiceScopeFactory serviceScopeFactory) + public CrashReportProcessorJob(ILogger logger, ICrashReporterClient crashReporterClient, IServiceScopeFactory serviceScopeFactory) + { + _logger = logger; + _crashReporterClient = crashReporterClient; + _serviceScopeFactory = serviceScopeFactory; + } + + public async Task Execute(IJobExecutionContext context) + { + using var ctsTimeout = new CancellationTokenSource(TimeSpan.FromMinutes(100)); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken, ctsTimeout.Token); + var ct = cts.Token; + + var processed = 0; + foreach (var tenant in TenantId.Values) + { + await using var scope = _serviceScopeFactory.CreateAsyncScope().WithTenant(tenant); + var crashReportBatchedHandler = scope.ServiceProvider.GetRequiredService(); + await foreach (var batch in _crashReporterClient.GetNewCrashReportMetadatasAsync(DateTime.UtcNow.AddDays(-2), ct).OfType().ChunkAsync(100).WithCancellation(ct)) + processed += await crashReportBatchedHandler.HandleBatchAsync(batch, ct); + } + + context.Result = $"Processed {processed} crash reports"; + context.SetIsSuccess(true); + } +} +/* +[DisallowConcurrentExecution] +public sealed class CrashReportProcessor2Job : IJob +{ + private readonly ILogger _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; + + public CrashReportProcessor2Job(ILogger logger, IServiceScopeFactory serviceScopeFactory) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); @@ -42,15 +75,64 @@ public async Task Execute(IJobExecutionContext context) tenantContextAccessor.Current = tenant; var client = scope.ServiceProvider.GetRequiredService(); + + var dbContextFactory = scope.ServiceProvider.GetRequiredService(); + var dbContextRead = await dbContextFactory.CreateReadAsync(ct); + var dbContextWrite = await dbContextFactory.CreateWriteAsync(ct); + + var offset = 0; + while (true) + { + var data = dbContextRead.CrashReports + .IgnoreAutoIncludes() + .Include(x => x.FileId) + .Include(x => x.ExceptionType) + .Where(x => x.TenantId == tenant) + .OrderBy(x => x.CreatedAt) + .Skip(offset) + .Take(5000) + .ToList(); + + var entityFactory = dbContextWrite.GetEntityFactory(); + await using var __ = await dbContextWrite.CreateSaveScopeAsync(); + + var toChange = new List(); + foreach (var crashReportEntity in data) + { + if (!ExceptionTypeId.TryParseFromException(crashReportEntity.Exception, out var exception)) continue; + if (crashReportEntity.ExceptionType.Id == exception) continue; + + //CrashReportModel? model; + //try + //{ + // var content = await client.GetCrashReportAsync(crashReportEntity.FileId!.FileId, ct); + // CrashReportParser.TryParse(content, out _, out model, out _); + //} + //catch (Exception e) + //{ + // model = await client.GetCrashReportModelAsync(crashReportEntity.FileId!.FileId, ct); + //} + //var exceptionTypeEntity = entityFactory.GetOrCreateExceptionType(ExceptionTypeId.FromException(model!.Exception)); + toChange.Add(crashReportEntity with { ExceptionType = entityFactory.GetOrCreateExceptionType(exception) }); + } + + await dbContextWrite.CrashReports.UpsertOnSaveAsync(toChange); + + offset += data.Count; + if (data.Count == 0) break; + } + + await using var crashReportBatchedHandler = scope.ServiceProvider.GetRequiredService(); await foreach (var batch in client.GetNewCrashReportMetadatasAsync(DateTime.UtcNow.AddDays(-2), ct).OfType().ChunkAsync(1000).WithCancellation(ct)) { - processed += await crashReportBatchedHandler.HandleBatchAsync(batch, ct); + processed += await crashReportBatchedHandler.HandleBatchAsync(tenant, batch, ct); } } context.Result = $"Processed {processed} crash reports"; context.SetIsSuccess(true); } -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsArticleProcessorJob.cs b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsArticleProcessorJob.cs index 61612157..d6f08449 100644 --- a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsArticleProcessorJob.cs +++ b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsArticleProcessorJob.cs @@ -1,7 +1,7 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Services; using Microsoft.Extensions.DependencyInjection; @@ -21,12 +21,14 @@ namespace BUTR.Site.NexusMods.Server.Jobs; public sealed class NexusModsArticleProcessorJob : IJob { private readonly ILogger _logger; + private readonly INexusModsClient _nexusModsClient; private readonly IServiceScopeFactory _serviceScopeFactory; - public NexusModsArticleProcessorJob(ILogger logger, IServiceScopeFactory serviceScopeFactory) + public NexusModsArticleProcessorJob(ILogger logger, INexusModsClient nexusModsClient, IServiceScopeFactory serviceScopeFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + _logger = logger; + _nexusModsClient = nexusModsClient; + _serviceScopeFactory = serviceScopeFactory; } public async Task Execute(IJobExecutionContext context) @@ -37,25 +39,20 @@ public async Task Execute(IJobExecutionContext context) foreach (var tenant in TenantId.Values) { - await using var scope = _serviceScopeFactory.CreateAsyncScope(); - - var tenantContextAccessor = scope.ServiceProvider.GetRequiredService(); - tenantContextAccessor.Current = tenant; - - await HandleTenantAsync(tenant, scope.ServiceProvider, ct); + await using var scope = _serviceScopeFactory.CreateAsyncScope().WithTenant(tenant); + await HandleTenantAsync(scope, tenant, ct); } context.Result = "Processed all available articles"; context.SetIsSuccess(true); } - private static async Task HandleTenantAsync(TenantId tenant, IServiceProvider serviceProvider, CancellationToken ct) + private async Task HandleTenantAsync(AsyncServiceScope scope, TenantId tenant, CancellationToken ct) { const int notFoundArticlesTreshold = 50; - var client = serviceProvider.GetRequiredService(); - var dbContextWrite = serviceProvider.GetRequiredService(); - var entityFactory = dbContextWrite.GetEntityFactory(); + var unitOfWorkFactory = scope.ServiceProvider.GetRequiredService(); + await using var unitOfWrite = unitOfWorkFactory.CreateUnitOfWrite(); var gameDomain = tenant.ToGameDomain(); @@ -64,14 +61,13 @@ private static async Task HandleTenantAsync(TenantId tenant, IServiceProvider se var @break = false; while (!ct.IsCancellationRequested && !@break) { - await using var _ = await dbContextWrite.CreateSaveScopeAsync(); var articles = ImmutableArray.CreateBuilder(); for (var i = 0; i < 50; i++) { var articleId = NexusModsArticleId.From(articleIdRaw); - if (await client.GetArticleAsync(gameDomain, articleId, ct) is not { } articleDocument) + if (await _nexusModsClient.GetArticleAsync(gameDomain, articleId, ct) is not { } articleDocument) { articleIdRaw++; continue; @@ -112,14 +108,15 @@ private static async Task HandleTenantAsync(TenantId tenant, IServiceProvider se TenantId = tenant, Title = title, NexusModsArticleId = articleId, - NexusModsUser = entityFactory.GetOrCreateNexusModsUserWithName(authorId, NexusModsUserName.From(authorText)), + NexusModsUserId = authorId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUserWithName(authorId, NexusModsUserName.From(authorText)), CreateDate = dateTime }); articleIdRaw++; } - await dbContextWrite.NexusModsArticles.UpsertOnSaveAsync(articles.ToArray()); - // Disposing the DBContext will save the data + unitOfWrite.NexusModsArticles.UpsertRange(articles); + await unitOfWrite.SaveChangesAsync(CancellationToken.None); } } } \ 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 34fa0473..eec3f59e 100644 --- a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsArticleUpdatesProcessorJob.cs +++ b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsArticleUpdatesProcessorJob.cs @@ -1,7 +1,7 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Services; using Microsoft.Extensions.DependencyInjection; @@ -21,12 +21,14 @@ namespace BUTR.Site.NexusMods.Server.Jobs; public sealed class NexusModsArticleUpdatesProcessorJob : IJob { private readonly ILogger _logger; + private readonly INexusModsClient _nexusModsClient; private readonly IServiceScopeFactory _serviceScopeFactory; - public NexusModsArticleUpdatesProcessorJob(ILogger logger, IServiceScopeFactory serviceScopeFactory) + public NexusModsArticleUpdatesProcessorJob(ILogger logger, INexusModsClient nexusModsClient, IServiceScopeFactory serviceScopeFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + _logger = logger; + _nexusModsClient = nexusModsClient; + _serviceScopeFactory = serviceScopeFactory; } public async Task Execute(IJobExecutionContext context) @@ -38,40 +40,35 @@ public async Task Execute(IJobExecutionContext context) var processed = 0; foreach (var tenant in TenantId.Values) { - await using var scope = _serviceScopeFactory.CreateAsyncScope(); - - var tenantContextAccessor = scope.ServiceProvider.GetRequiredService(); - tenantContextAccessor.Current = tenant; - - processed += await HandleTenantAsync(tenant, scope.ServiceProvider, ct); + await using var scope = _serviceScopeFactory.CreateAsyncScope().WithTenant(tenant); + processed += await HandleTenantAsync(scope, tenant, ct); } context.Result = $"Processed {processed} article updates"; context.SetIsSuccess(true); } - private static async Task HandleTenantAsync(TenantId tenant, IServiceProvider serviceProvider, CancellationToken ct) + private async Task HandleTenantAsync(AsyncServiceScope scope, TenantId tenant, CancellationToken ct) { const int notFoundArticlesTreshold = 50; - var client = serviceProvider.GetRequiredService(); - var dbContextRead = serviceProvider.GetRequiredService(); - var dbContextWrite = serviceProvider.GetRequiredService(); - var entityFactory = dbContextWrite.GetEntityFactory(); - await using var _ = await dbContextWrite.CreateSaveScopeAsync(); + var unitOfWorkFactory = scope.ServiceProvider.GetRequiredService(); + await using var unitOfRead = unitOfWorkFactory.CreateUnitOfRead(); + await using var unitOfWrite = unitOfWorkFactory.CreateUnitOfWrite(); var gameDomain = tenant.ToGameDomain(); var articles = ImmutableArray.CreateBuilder(); - var articleIdRaw = dbContextRead.NexusModsArticles.OrderBy(x => x.NexusModsArticleId).LastOrDefault()?.NexusModsArticleId.Value ?? 0; + var lastArticle = await unitOfRead.NexusModsArticles.LastOrDefaultAsync(null, x => x.OrderBy(y => y.NexusModsArticleId), ct); + var articleIdRaw = lastArticle?.NexusModsArticleId.Value ?? 0; var notFoundArticles = 0; var processed = 0; while (!ct.IsCancellationRequested) { var articleId = NexusModsArticleId.From(articleIdRaw); - if (await client.GetArticleAsync(gameDomain, articleId, ct) is not { } articleDocument) + if (await _nexusModsClient.GetArticleAsync(gameDomain, articleId, ct) is not { } articleDocument) { articleIdRaw++; continue; @@ -111,16 +108,17 @@ private static async Task HandleTenantAsync(TenantId tenant, IServiceProvid TenantId = tenant, Title = title, NexusModsArticleId = articleId, - NexusModsUser = entityFactory.GetOrCreateNexusModsUserWithName(authorId, NexusModsUserName.From(authorText)), + NexusModsUserId = authorId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUserWithName(authorId, NexusModsUserName.From(authorText)), CreateDate = dateTime }); articleIdRaw++; processed++; } - await dbContextWrite.NexusModsArticles.UpsertOnSaveAsync(articles.ToArray()); - // Disposing the DBContext will save the data + unitOfWrite.NexusModsArticles.UpsertRange(articles); + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return processed; } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileProcessorJob.cs b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileProcessorJob.cs index 7cb3c10a..73e6c1bc 100644 --- a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileProcessorJob.cs +++ b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileProcessorJob.cs @@ -1,8 +1,8 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.Database; using BUTR.Site.NexusMods.Server.Options; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Services; using Microsoft.Extensions.DependencyInjection; @@ -27,12 +27,18 @@ namespace BUTR.Site.NexusMods.Server.Jobs; public sealed class NexusModsModFileProcessorJob : IJob { private readonly ILogger _logger; + private readonly NexusModsOptions _nexusModsOptions; + private readonly INexusModsAPIClient _nexusModsAPIClient; + private readonly INexusModsModFileParser _nexusModsModFileParser; private readonly IServiceScopeFactory _serviceScopeFactory; - public NexusModsModFileProcessorJob(ILogger logger, IServiceScopeFactory serviceScopeFactory) + public NexusModsModFileProcessorJob(ILogger logger, IOptions nexusModsOptions, INexusModsAPIClient nexusModsAPIClient, INexusModsModFileParser nexusModsModFileParser, IServiceScopeFactory serviceScopeFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + _logger = logger; + _nexusModsOptions = nexusModsOptions.Value; + _nexusModsAPIClient = nexusModsAPIClient; + _nexusModsModFileParser = nexusModsModFileParser; + _serviceScopeFactory = serviceScopeFactory; } public async Task Execute(IJobExecutionContext context) @@ -45,12 +51,8 @@ public async Task Execute(IJobExecutionContext context) var exceptions = new List(); foreach (var tenant in TenantId.Values) { - await using var scope = _serviceScopeFactory.CreateAsyncScope(); - - var tenantContextAccessor = scope.ServiceProvider.GetRequiredService(); - tenantContextAccessor.Current = tenant; - - var (processed_, exceptions_) = await HandleTenantAsync(tenant, scope.ServiceProvider, ct); + await using var scope = _serviceScopeFactory.CreateAsyncScope().WithTenant(tenant); + var (processed_, exceptions_) = await HandleTenantAsync(scope, tenant, ct); processed += processed_; exceptions.AddRange(exceptions_); } @@ -59,15 +61,12 @@ public async Task Execute(IJobExecutionContext context) context.SetIsSuccess(exceptions.Count == 0); } - private static async Task<(int Processed, List Exceptions)> HandleTenantAsync(TenantId tenant, IServiceProvider serviceProvider, CancellationToken ct) + private async Task<(int Processed, List Exceptions)> HandleTenantAsync(AsyncServiceScope scope, TenantId tenant, CancellationToken ct) { const int notFoundModsTreshold = 25; - var info = serviceProvider.GetRequiredService(); - var options = serviceProvider.GetRequiredService>().Value; - var client = serviceProvider.GetRequiredService(); - var dbContextWrite = serviceProvider.GetRequiredService(); - var entityFactory = dbContextWrite.GetEntityFactory(); + var unitOfWorkFactory = scope.ServiceProvider.GetRequiredService(); + await using var unitOfWrite = unitOfWorkFactory.CreateUnitOfWrite(); var gameDomain = tenant.ToGameDomain(); @@ -78,6 +77,7 @@ public async Task Execute(IJobExecutionContext context) var modIdRaw = 1; //2907, 5090 while (!ct.IsCancellationRequested) { + var modId = NexusModsModId.From(modIdRaw); var nexusModsModModuleEntities = ImmutableArray.CreateBuilder(); @@ -86,7 +86,8 @@ public async Task Execute(IJobExecutionContext context) try { - var response = await client.GetModFileInfosFullAsync(gameDomain, modId, options.ApiKey, ct); + var response = await _nexusModsAPIClient + .GetModFileInfosFullAsync(gameDomain, modId, _nexusModsOptions.ApiKey, ct); if (response is null) { notFoundMods++; @@ -96,40 +97,44 @@ public async Task Execute(IJobExecutionContext context) continue; } - var infos = await info.GetModuleInfosAsync(gameDomain, modId, response.Files, options.ApiKey, ct).ToArrayAsync(ct); + var infos = await _nexusModsModFileParser.GetModuleInfosAsync(gameDomain, modId, response.Files, _nexusModsOptions.ApiKey, ct).ToArrayAsync(ct); var latestFileUpdate = DateTimeOffset.FromUnixTimeSeconds(response.Files.Select(x => x.UploadedTimestamp).Where(x => x is not null).Max() ?? 0).ToUniversalTime(); nexusModsModModuleEntities.AddRange(infos.Select(x => x.ModuleInfo).DistinctBy(x => x.Id).Select(x => new NexusModsModToModuleEntity { TenantId = tenant, - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(modId), - Module = entityFactory.GetOrCreateModule(ModuleId.From(x.Id)), + NexusModsModId = modId, + NexusModsMod = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modId), + ModuleId = ModuleId.From(x.Id), + Module = unitOfWrite.UpsertEntityFactory.GetOrCreateModule(ModuleId.From(x.Id)), LastUpdateDate = latestFileUpdate, LinkType = NexusModsModToModuleLinkType.ByUnverifiedFileExposure, - })); + }).ToList()); nexusModsModToFileUpdateEntities.Add(new NexusModsModToFileUpdateEntity { TenantId = tenant, - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(modId), + NexusModsModId = modId, + NexusModsMod = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modId), LastCheckedDate = latestFileUpdate, }); nexusModsModToModuleInfoHistoryEntities.AddRange(infos.DistinctBy(x => new { x.ModuleInfo.Id, x.ModuleInfo.Version, x.FileId }).Select(x => new NexusModsModToModuleInfoHistoryEntity { TenantId = tenant, NexusModsFileId = x.FileId, - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(modId), - Module = entityFactory.GetOrCreateModule(ModuleId.From(x.ModuleInfo.Id)), + NexusModsModId = modId, + NexusModsMod = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modId), + ModuleId = ModuleId.From(x.ModuleInfo.Id), + Module = unitOfWrite.UpsertEntityFactory.GetOrCreateModule(ModuleId.From(x.ModuleInfo.Id)), ModuleVersion = ModuleVersion.From(x.ModuleInfo.Version.ToString()), ModuleInfo = ModuleInfoModel.Create(x.ModuleInfo), UploadDate = x.Uploaded, - })); + }).ToList()); - await using var _ = await dbContextWrite.CreateSaveScopeAsync(); - await dbContextWrite.NexusModsModModules.UpsertOnSaveAsync(nexusModsModModuleEntities.ToArray()); - await dbContextWrite.NexusModsModToFileUpdates.UpsertOnSaveAsync(nexusModsModToFileUpdateEntities.ToArray()); - await dbContextWrite.NexusModsModToModuleInfoHistory.UpsertOnSaveAsync(nexusModsModToModuleInfoHistoryEntities.ToArray()); - // Disposing the DBContext will save the data + unitOfWrite.NexusModsModModules.UpsertRange(nexusModsModModuleEntities); + unitOfWrite.NexusModsModToFileUpdates.UpsertRange(nexusModsModToFileUpdateEntities); + unitOfWrite.NexusModsModToModuleInfoHistory.UpsertRange(nexusModsModToModuleInfoHistoryEntities); + await unitOfWrite.SaveChangesAsync(ct); processed++; } catch (Exception e) diff --git a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileUpdatesProcessorJob.cs b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileUpdatesProcessorJob.cs index 90a3c8ca..0bb2d3e6 100644 --- a/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileUpdatesProcessorJob.cs +++ b/src/BUTR.Site.NexusMods.Server/Jobs/NexusModsModFileUpdatesProcessorJob.cs @@ -1,12 +1,11 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.Database; using BUTR.Site.NexusMods.Server.Models.NexusModsAPI; using BUTR.Site.NexusMods.Server.Options; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Services; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -28,12 +27,18 @@ namespace BUTR.Site.NexusMods.Server.Jobs; public sealed class NexusModsModFileUpdatesProcessorJob : IJob { private readonly ILogger _logger; + private readonly NexusModsOptions _nexusModsOptions; + private readonly INexusModsAPIClient _nexusModsAPIClient; + private readonly INexusModsModFileParser _nexusModsModFileParser; private readonly IServiceScopeFactory _serviceScopeFactory; - public NexusModsModFileUpdatesProcessorJob(ILogger logger, IServiceScopeFactory serviceScopeFactory) + public NexusModsModFileUpdatesProcessorJob(ILogger logger, IOptions nexusModsOptions, INexusModsAPIClient nexusModsAPIClient, INexusModsModFileParser nexusModsModFileParser, IServiceScopeFactory serviceScopeFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + _logger = logger; + _nexusModsOptions = nexusModsOptions.Value; + _nexusModsAPIClient = nexusModsAPIClient; + _nexusModsModFileParser = nexusModsModFileParser; + _serviceScopeFactory = serviceScopeFactory; } public async Task Execute(IJobExecutionContext context) @@ -49,12 +54,8 @@ public async Task Execute(IJobExecutionContext context) var newUpdates = 0; foreach (var tenant in TenantId.Values) { - await using var scope = _serviceScopeFactory.CreateAsyncScope(); - - var tenantContextAccessor = scope.ServiceProvider.GetRequiredService(); - tenantContextAccessor.Current = tenant; - - var (processed_, exceptions_, updatesStoredWithinDay_, updatedWithinDay_, newUpdates_) = await HandleTenantAsync(tenant, scope.ServiceProvider, ct); + await using var scope = _serviceScopeFactory.CreateAsyncScope().WithTenant(tenant); + var (processed_, exceptions_, updatesStoredWithinDay_, updatedWithinDay_, newUpdates_) = await HandleTenantAsync(scope, tenant, ct); processed += processed_; exceptions.AddRange(exceptions_); updatesStoredWithinDay += updatesStoredWithinDay_; @@ -70,18 +71,17 @@ public async Task Execute(IJobExecutionContext context) context.SetIsSuccess(exceptions.Count == 0); } - private static async Task<(int Processed, List Exceptions, int UpdatesStoredWithinDay, int UpdatedWithinDay, int NewUpdates)> HandleTenantAsync(TenantId tenant, IServiceProvider serviceProvider, CancellationToken ct) + private async Task<(int Processed, List Exceptions, int UpdatesStoredWithinDay, int UpdatedWithinDay, int NewUpdates)> HandleTenantAsync(AsyncServiceScope scope, TenantId tenant, CancellationToken ct) { - var gameDomain = tenant.ToGameDomain(); + var unitOfWorkFactory = scope.ServiceProvider.GetRequiredService(); + await using var unitOfRead = unitOfWorkFactory.CreateUnitOfRead(); + await using var unitOfWrite = unitOfWorkFactory.CreateUnitOfWrite(); - var info = serviceProvider.GetRequiredService(); - var options = serviceProvider.GetRequiredService>().Value; - var client = serviceProvider.GetRequiredService(); - var dbContextRead = serviceProvider.GetRequiredService(); + var gameDomain = tenant.ToGameDomain(); - var dateOneWeekAgo = DateTime.UtcNow.AddDays(-7); - var updatesStoredWithinWeek = await dbContextRead.NexusModsModToFileUpdates.Where(x => x.LastCheckedDate > dateOneWeekAgo).ToListAsync(ct); - var updatedWithinWeek = await client.GetAllModUpdatesWeekAsync(gameDomain, options.ApiKey, ct) ?? Array.Empty(); + var dateOneWeekAgo = DateTime.UtcNow.AddDays(-30); + var updatesStoredWithinWeek = await unitOfRead.NexusModsModToFileUpdates.GetAllAsync(x => x.LastCheckedDate > dateOneWeekAgo, null, ct); + var updatedWithinWeek = await _nexusModsAPIClient.GetAllModUpdatesWeekAsync(gameDomain, _nexusModsOptions.ApiKey, ct) ?? Array.Empty(); var newUpdates = updatedWithinWeek.Where(x => { var latestFileUpdateDate = DateTimeOffset.FromUnixTimeSeconds(x.LatestFileUpdateTimestamp).ToUniversalTime(); @@ -95,45 +95,49 @@ public async Task Execute(IJobExecutionContext context) var exceptions = new List(); foreach (var modUpdate in newUpdates) { - var dbContextWrite = serviceProvider.GetRequiredService(); - var entityFactory = dbContextWrite.GetEntityFactory(); - await using var _ = await dbContextWrite.CreateSaveScopeAsync(); try { if (ct.IsCancellationRequested) break; - if (await client.GetModFileInfosFullAsync(gameDomain, modUpdate.Id, options.ApiKey, ct) is not { } response) continue; + if (await _nexusModsAPIClient.GetModFileInfosFullAsync(gameDomain, modUpdate.Id, _nexusModsOptions.ApiKey, ct) is not { } response) continue; var updates = response.FileUpdates.Where(x => DateTimeOffset.FromUnixTimeSeconds(x.UploadedTimestamp) > dateOneWeekAgo).ToArray(); if (updates.Length == 0) continue; - var infos = await info.GetModuleInfosAsync(gameDomain, modUpdate.Id, response.Files.Where(x => updates.Any(y => y.NewId == x.FileId)), options.ApiKey, ct).ToArrayAsync(ct); + var infos = await _nexusModsModFileParser.GetModuleInfosAsync(gameDomain, modUpdate.Id, response.Files.Where(x => updates.Any(y => y.NewId == x.FileId)), _nexusModsOptions.ApiKey, ct).ToArrayAsync(ct); var lastUpdateTime = DateTimeOffset.FromUnixTimeSeconds(modUpdate.LatestFileUpdateTimestamp).ToUniversalTime(); - await dbContextWrite.NexusModsModModules.UpsertOnSaveAsync(infos.Select(x => x.ModuleInfo).DistinctBy(x => x.Id).Select(x => new NexusModsModToModuleEntity + unitOfWrite.NexusModsModModules.UpsertRange(infos.Select(x => x.ModuleInfo).DistinctBy(x => x.Id).Select(x => new NexusModsModToModuleEntity { TenantId = tenant, - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(modUpdate.Id), - Module = entityFactory.GetOrCreateModule(ModuleId.From(x.Id)), + NexusModsModId = modUpdate.Id, + NexusModsMod = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modUpdate.Id), + ModuleId = ModuleId.From(x.Id), + Module = unitOfWrite.UpsertEntityFactory.GetOrCreateModule(ModuleId.From(x.Id)), LastUpdateDate = lastUpdateTime, LinkType = NexusModsModToModuleLinkType.ByUnverifiedFileExposure - })); - await dbContextWrite.NexusModsModToFileUpdates.UpsertOnSaveAsync(new NexusModsModToFileUpdateEntity + }).ToList()); + unitOfWrite.NexusModsModToFileUpdates.Upsert(new NexusModsModToFileUpdateEntity { TenantId = tenant, - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(modUpdate.Id), + NexusModsModId = modUpdate.Id, + NexusModsMod = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modUpdate.Id), LastCheckedDate = lastUpdateTime }); - await dbContextWrite.NexusModsModToModuleInfoHistory.UpsertOnSaveAsync(infos.DistinctBy(x => new { x.ModuleInfo.Id, x.ModuleInfo.Version, x.FileId }).Select(x => new NexusModsModToModuleInfoHistoryEntity + unitOfWrite.NexusModsModToModuleInfoHistory.UpsertRange(infos.DistinctBy(x => new { x.ModuleInfo.Id, x.ModuleInfo.Version, x.FileId }).Select(x => new NexusModsModToModuleInfoHistoryEntity { TenantId = tenant, NexusModsFileId = x.FileId, - NexusModsMod = entityFactory.GetOrCreateNexusModsMod(modUpdate.Id), - Module = entityFactory.GetOrCreateModule(ModuleId.From(x.ModuleInfo.Id)), + NexusModsModId = modUpdate.Id, + NexusModsMod = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modUpdate.Id), + ModuleId = ModuleId.From(x.ModuleInfo.Id), + Module = unitOfWrite.UpsertEntityFactory.GetOrCreateModule(ModuleId.From(x.ModuleInfo.Id)), ModuleVersion = ModuleVersion.From(x.ModuleInfo.Version.ToString()), ModuleInfo = ModuleInfoModel.Create(x.ModuleInfo), UploadDate = x.Uploaded, - })); + }).ToList()); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); processed++; } catch (Exception e) diff --git a/src/BUTR.Site.NexusMods.Server/Jobs/QuartzLogHistoryManagerExecutionLogsJob.cs b/src/BUTR.Site.NexusMods.Server/Jobs/QuartzLogHistoryManagerExecutionLogsJob.cs index 1d871347..cff30eef 100644 --- a/src/BUTR.Site.NexusMods.Server/Jobs/QuartzLogHistoryManagerExecutionLogsJob.cs +++ b/src/BUTR.Site.NexusMods.Server/Jobs/QuartzLogHistoryManagerExecutionLogsJob.cs @@ -1,12 +1,12 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; +using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Repositories; -using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using Quartz; using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -14,20 +14,24 @@ namespace BUTR.Site.NexusMods.Server.Jobs; public sealed class QuartzLogHistoryManagerExecutionLogsJob : IJob { - private readonly IAppDbContextWrite _dbContextWrite; + private readonly IServiceScopeFactory _serviceScopeFactory; - public QuartzLogHistoryManagerExecutionLogsJob(IAppDbContextWrite dbContextWrite) + public QuartzLogHistoryManagerExecutionLogsJob(IServiceScopeFactory serviceScopeFactory) { - _dbContextWrite = dbContextWrite ?? throw new ArgumentNullException(nameof(dbContextWrite)); + _serviceScopeFactory = serviceScopeFactory; } public async Task Execute(IJobExecutionContext context) { - var ct = CancellationToken.None; + await using var scope = _serviceScopeFactory.CreateAsyncScope().WithTenant(TenantId.None); - var count = await _dbContextWrite.QuartzExecutionLogs - .Where(x => DateTimeOffset.UtcNow - x.DateAddedUtc > TimeSpan.FromDays(30)) - .ExecuteDeleteAsync(ct); + var unitOfWorkFactory = scope.ServiceProvider.GetRequiredService(); + await using var unitOfWrite = unitOfWorkFactory.CreateUnitOfWrite(); + + var count = unitOfWrite.QuartzExecutionLogs + .Remove(x => DateTimeOffset.UtcNow - x.DateAddedUtc > TimeSpan.FromDays(30)); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); context.Result = $"Deleted {count} execution logs"; context.SetIsSuccess(true); diff --git a/src/BUTR.Site.NexusMods.Server/Jobs/TopExceptionsTypesAnalyzerProcessorJob.cs b/src/BUTR.Site.NexusMods.Server/Jobs/TopExceptionsTypesAnalyzerProcessorJob.cs index b408f675..ac234280 100644 --- a/src/BUTR.Site.NexusMods.Server/Jobs/TopExceptionsTypesAnalyzerProcessorJob.cs +++ b/src/BUTR.Site.NexusMods.Server/Jobs/TopExceptionsTypesAnalyzerProcessorJob.cs @@ -1,9 +1,8 @@ -using BUTR.Site.NexusMods.Server.Contexts; using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -24,8 +23,8 @@ public sealed class TopExceptionsTypesAnalyzerProcessorJob : IJob public TopExceptionsTypesAnalyzerProcessorJob(ILogger logger, IServiceScopeFactory serviceScopeFactory) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + _logger = logger; + _serviceScopeFactory = serviceScopeFactory; } public async Task Execute(IJobExecutionContext context) @@ -36,33 +35,32 @@ public async Task Execute(IJobExecutionContext context) foreach (var tenant in TenantId.Values) { - await using var scope = _serviceScopeFactory.CreateAsyncScope(); - - var tenantContextAccessor = scope.ServiceProvider.GetRequiredService(); - tenantContextAccessor.Current = tenant; - - await HandleTenantAsync(tenant, scope.ServiceProvider, ct); + await using var scope = _serviceScopeFactory.CreateAsyncScope().WithTenant(tenant); + await HandleTenantAsync(scope, tenant, ct); } context.Result = "Updated Top Exception Types"; context.SetIsSuccess(true); } - private static async Task HandleTenantAsync(TenantId tenant, IServiceProvider serviceProvider, CancellationToken ct) + private async Task HandleTenantAsync(AsyncServiceScope scope, TenantId tenant, CancellationToken ct) { - var dbContextRead = serviceProvider.GetRequiredService(); - var dbContextWrite = serviceProvider.GetRequiredService(); - var entityFactory = dbContextWrite.GetEntityFactory(); - await using var _ = await dbContextWrite.CreateSaveScopeAsync(); + var unitOfWorkFactory = scope.ServiceProvider.GetRequiredService(); + await using var unitOfRead = unitOfWorkFactory.CreateUnitOfRead(); + await using var unitOfWrite = unitOfWorkFactory.CreateUnitOfWrite(); - var statisticsQuery = await dbContextRead.ExceptionTypes.Include(x => x.ToCrashReports).AsSplitQuery().Select(x => new StatisticsTopExceptionsTypeEntity + var exceptionTypes = await unitOfRead.ExceptionTypes.GetAllAsync(null, null, ct); + var statistics = exceptionTypes.Select(x => new StatisticsTopExceptionsTypeEntity { TenantId = tenant, - ExceptionType = entityFactory.GetOrCreateExceptionType(x.ExceptionTypeId), + ExceptionTypeId = x.ExceptionTypeId, + ExceptionType = unitOfWrite.UpsertEntityFactory.GetOrCreateExceptionType(x.ExceptionTypeId), ExceptionCount = x.ToCrashReports.Count - }).ToArrayAsync(ct); + }).ToList(); + + unitOfWrite.StatisticsTopExceptionsTypes.Remove(x => true); + unitOfWrite.StatisticsTopExceptionsTypes.UpsertRange(statistics); - await dbContextWrite.StatisticsTopExceptionsTypes.SynchronizeOnSaveAsync(statisticsQuery); - // Disposing the DBContext will save the data + await unitOfWrite.SaveChangesAsync(CancellationToken.None); } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/AutocompleteEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/AutocompleteEntity.cs index 855a909b..e221f25b 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/AutocompleteEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/AutocompleteEntity.cs @@ -6,8 +6,9 @@ public sealed record AutocompleteEntity : IEntityWithTenant { public required TenantId TenantId { get; init; } + public required int AutocompleteId { get; init; } public required string Type { get; init; } public required string Value { get; init; } - public override int GetHashCode() => HashCode.Combine(TenantId, Type, Value); + public override int GetHashCode() => HashCode.Combine(TenantId, AutocompleteId, Type, Value); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportEntity.cs index b9c8f249..da0f42a7 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportEntity.cs @@ -17,6 +17,7 @@ public sealed record CrashReportEntity : IEntityWithTenant public required GameVersion GameVersion { get; init; } + public required ExceptionTypeId ExceptionTypeId { get; init; } public required ExceptionTypeEntity ExceptionType { get; init; } public required string Exception { get; init; } @@ -25,5 +26,5 @@ public sealed record CrashReportEntity : IEntityWithTenant public required CrashReportUrl Url { get; init; } - public override int GetHashCode() => HashCode.Combine(CrashReportId, Version, GameVersion, ExceptionType, Exception, CreatedAt, Url); + public override int GetHashCode() => HashCode.Combine(TenantId, CrashReportId, Version, GameVersion, ExceptionTypeId, Exception, CreatedAt, Url); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportIgnoredFileEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportIgnoredFileEntity.cs index 7e2df0e1..3cb798c9 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportIgnoredFileEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportIgnoredFileEntity.cs @@ -6,7 +6,7 @@ public sealed record CrashReportIgnoredFileEntity : IEntityWithTenant { public required TenantId TenantId { get; init; } - public required CrashReportFileId Value { get; init; } + public required CrashReportFileId CrashReportFileId { get; init; } - public override int GetHashCode() => HashCode.Combine(TenantId, Value); + public override int GetHashCode() => HashCode.Combine(TenantId, CrashReportFileId); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToFileIdEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToFileIdEntity.cs index 5f0e2cf9..5590cedd 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToFileIdEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToFileIdEntity.cs @@ -11,5 +11,5 @@ public sealed record CrashReportToFileIdEntity : IEntityWithTenant public required CrashReportFileId FileId { get; init; } - public override int GetHashCode() => HashCode.Combine(CrashReportId, FileId); + public override int GetHashCode() => HashCode.Combine(TenantId, CrashReportId, FileId); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToMetadataEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToMetadataEntity.cs index fe2799cb..fddcbd0e 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToMetadataEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToMetadataEntity.cs @@ -19,5 +19,5 @@ public sealed record CrashReportToMetadataEntity : IEntityWithTenant public required string? BLSEVersion { get; init; } public required string? LauncherExVersion { get; init; } - public override int GetHashCode() => HashCode.Combine(CrashReportId, LauncherType, LauncherVersion, Runtime, BUTRLoaderVersion, BLSEVersion, LauncherExVersion); + public override int GetHashCode() => HashCode.Combine(TenantId, CrashReportId, LauncherType, LauncherVersion, Runtime, BUTRLoaderVersion, BLSEVersion, LauncherExVersion); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToModuleMetadataEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToModuleMetadataEntity.cs index 68a6f6cc..4b824fae 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToModuleMetadataEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToModuleMetadataEntity.cs @@ -9,15 +9,17 @@ public sealed record CrashReportToModuleMetadataEntity : IEntityWithTenant public required CrashReportId CrashReportId { get; init; } public CrashReportEntity? ToCrashReport { get; init; } + public required ModuleId ModuleId { get; init; } public required ModuleEntity Module { get; init; } public required ModuleVersion Version { get; init; } + public required NexusModsModId? NexusModsModId { get; init; } public required NexusModsModEntity? NexusModsMod { get; init; } public required byte InvolvedPosition { get; init; } public required bool IsInvolved { get; init; } - public override int GetHashCode() => HashCode.Combine(CrashReportId, Module.ModuleId, Version, NexusModsMod?.NexusModsModId, InvolvedPosition, IsInvolved); + public override int GetHashCode() => HashCode.Combine(TenantId, CrashReportId, ModuleId, Version, NexusModsModId, InvolvedPosition, IsInvolved); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationDiscordTokensEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationDiscordTokensEntity.cs index 5a5dbbef..9ec7b280 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationDiscordTokensEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationDiscordTokensEntity.cs @@ -4,6 +4,7 @@ namespace BUTR.Site.NexusMods.Server.Models.Database; public sealed record IntegrationDiscordTokensEntity : IEntity { + public required NexusModsUserId NexusModsUserId { get; init; } public required NexusModsUserEntity NexusModsUser { get; init; } public required string DiscordUserId { get; init; } @@ -19,5 +20,5 @@ public sealed record IntegrationDiscordTokensEntity : IEntity public required string AccessToken { get; init; } public required DateTimeOffset AccessTokenExpiresAt { get; init; } - public override int GetHashCode() => HashCode.Combine(NexusModsUser.NexusModsUserId, DiscordUserId, RefreshToken, AccessToken, AccessTokenExpiresAt); + public override int GetHashCode() => HashCode.Combine(NexusModsUserId, DiscordUserId, RefreshToken, AccessToken, AccessTokenExpiresAt); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationGOGTokensEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationGOGTokensEntity.cs index 71b059bd..5dd4b68e 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationGOGTokensEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationGOGTokensEntity.cs @@ -4,6 +4,7 @@ namespace BUTR.Site.NexusMods.Server.Models.Database; public sealed record IntegrationGOGTokensEntity : IEntity { + public required NexusModsUserId NexusModsUserId { get; init; } public required NexusModsUserEntity NexusModsUser { get; init; } public required string GOGUserId { get; init; } @@ -19,5 +20,5 @@ public sealed record IntegrationGOGTokensEntity : IEntity public required string AccessToken { get; init; } public required DateTimeOffset AccessTokenExpiresAt { get; init; } - public override int GetHashCode() => HashCode.Combine(NexusModsUser.NexusModsUserId, GOGUserId, RefreshToken, AccessToken, AccessTokenExpiresAt); + public override int GetHashCode() => HashCode.Combine(NexusModsUserId, GOGUserId, RefreshToken, AccessToken, AccessTokenExpiresAt); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationGitHubTokensEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationGitHubTokensEntity.cs index 299956cc..91aebc19 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationGitHubTokensEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationGitHubTokensEntity.cs @@ -5,6 +5,7 @@ namespace BUTR.Site.NexusMods.Server.Models.Database; public sealed record IntegrationGitHubTokensEntity : IEntity { + public required NexusModsUserId NexusModsUserId { get; init; } public required NexusModsUserEntity NexusModsUser { get; init; } public required string GitHubUserId { get; init; } @@ -12,5 +13,5 @@ public sealed record IntegrationGitHubTokensEntity : IEntity public required string AccessToken { get; init; } - public override int GetHashCode() => HashCode.Combine(NexusModsUser.NexusModsUserId, GitHubUserId, AccessToken); + public override int GetHashCode() => HashCode.Combine(NexusModsUserId, GitHubUserId, AccessToken); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationSteamTokensEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationSteamTokensEntity.cs index 102dcbe6..5fe2e21d 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationSteamTokensEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationSteamTokensEntity.cs @@ -5,6 +5,7 @@ namespace BUTR.Site.NexusMods.Server.Models.Database; public sealed record IntegrationSteamTokensEntity : IEntity { + public required NexusModsUserId NexusModsUserId { get; init; } public required NexusModsUserEntity NexusModsUser { get; init; } public required string SteamUserId { get; init; } @@ -12,5 +13,5 @@ public sealed record IntegrationSteamTokensEntity : IEntity public required Dictionary Data { get; init; } - public override int GetHashCode() => HashCode.Combine(NexusModsUser.NexusModsUserId, SteamUserId, Data); + public override int GetHashCode() => HashCode.Combine(NexusModsUserId, SteamUserId, Data); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsArticleEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsArticleEntity.cs index 5a972de2..9f497e51 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsArticleEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsArticleEntity.cs @@ -10,9 +10,10 @@ public sealed record NexusModsArticleEntity : IEntityWithTenant public required string Title { get; init; } + public required NexusModsUserId NexusModsUserId { get; init; } public required NexusModsUserEntity NexusModsUser { get; init; } public required DateTimeOffset CreateDate { get; init; } - public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsArticleId, Title, NexusModsUser.NexusModsUserId, CreateDate); + public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsArticleId, Title, NexusModsUserId, CreateDate); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModEntity.cs index d8eb8692..7ad45bcb 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModEntity.cs @@ -27,6 +27,7 @@ private NexusModsModEntity() { } private NexusModsModEntity(TenantId tenant, NexusModsModId modId, DateTimeOffset lastCheckedDate) : this(tenant, modId) => FileUpdate = new() { TenantId = tenant, + NexusModsModId = modId, NexusModsMod = this, LastCheckedDate = lastCheckedDate }; diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToFileUpdateEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToFileUpdateEntity.cs index 7919f4d3..a18317e1 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToFileUpdateEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToFileUpdateEntity.cs @@ -6,9 +6,10 @@ public sealed record NexusModsModToFileUpdateEntity : IEntityWithTenant { public required TenantId TenantId { get; init; } + public required NexusModsModId NexusModsModId { get; init; } public required NexusModsModEntity NexusModsMod { get; init; } public required DateTimeOffset LastCheckedDate { get; init; } - public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsMod.NexusModsModId, LastCheckedDate); + public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsModId, LastCheckedDate); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleEntity.cs index bbbaf298..808f0a19 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleEntity.cs @@ -6,13 +6,15 @@ public sealed record NexusModsModToModuleEntity : IEntityWithTenant { public required TenantId TenantId { get; init; } + public required NexusModsModId NexusModsModId { get; init; } public required NexusModsModEntity NexusModsMod { get; init; } + public required ModuleId ModuleId { get; init; } public required ModuleEntity Module { get; init; } public required NexusModsModToModuleLinkType LinkType { get; init; } public required DateTimeOffset LastUpdateDate { get; init; } - public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsMod.NexusModsModId, Module.ModuleId, LinkType, LastUpdateDate); + public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsModId, ModuleId, LinkType, LastUpdateDate); } \ 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 index 2afbeba5..3c3ac78f 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleInfoHistoryEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleInfoHistoryEntity.cs @@ -6,7 +6,9 @@ namespace BUTR.Site.NexusMods.Server.Models.Database; public sealed record NexusModsModToModuleInfoHistoryEntity : IEntityWithTenant { public required TenantId TenantId { get; init; } + public required NexusModsModId NexusModsModId { get; init; } public required NexusModsModEntity NexusModsMod { get; init; } + public required ModuleId ModuleId { get; init; } public required ModuleEntity Module { get; init; } public required ModuleVersion ModuleVersion { get; init; } public required NexusModsFileId NexusModsFileId { get; init; } @@ -17,5 +19,5 @@ public sealed record NexusModsModToModuleInfoHistoryEntity : IEntityWithTenant public ICollection GameVersions { get; init; } = new List(); - public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsMod.NexusModsModId, NexusModsFileId, Module.ModuleId, ModuleVersion); + public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsModId, ModuleId, ModuleVersion, NexusModsFileId, UploadDate); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleInfoHistoryGameVersionEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleInfoHistoryGameVersionEntity.cs index c3e81fc8..3e54cf54 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleInfoHistoryGameVersionEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleInfoHistoryGameVersionEntity.cs @@ -1,9 +1,13 @@ +using System; + namespace BUTR.Site.NexusMods.Server.Models.Database; public sealed record NexusModsModToModuleInfoHistoryGameVersionEntity : IEntityWithTenant { public required TenantId TenantId { get; init; } + public required NexusModsModId NexusModsModId { get; init; } public required NexusModsModEntity NexusModsMod { get; init; } + public required ModuleId ModuleId { get; init; } public required ModuleEntity Module { get; init; } public required ModuleVersion ModuleVersion { get; init; } public required NexusModsFileId NexusModsFileId { get; init; } @@ -11,4 +15,7 @@ public sealed record NexusModsModToModuleInfoHistoryGameVersionEntity : IEntityW public required GameVersion GameVersion { get; init; } public NexusModsModToModuleInfoHistoryEntity MainEntity { get; init; } + + public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsModId, ModuleId, ModuleVersion, NexusModsFileId, GameVersion); + } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToNameEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToNameEntity.cs index 009a89e2..d30b6b29 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToNameEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToNameEntity.cs @@ -6,9 +6,10 @@ public sealed record NexusModsModToNameEntity : IEntityWithTenant { public required TenantId TenantId { get; init; } + public required NexusModsModId NexusModsModId { get; init; } public required NexusModsModEntity NexusModsMod { get; init; } public required string Name { get; init; } - public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsMod.NexusModsModId, Name); + public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsModId, Name); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserEntity.cs index 4cc80c5d..91248ded 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserEntity.cs @@ -31,6 +31,7 @@ private NexusModsUserEntity(NexusModsUserId userId, NexusModsUserName? name = nu NexusModsUserId = userId; Name = name is { } nameVal ? new() { + NexusModsUserId = userId, NexusModsUser = this, Name = nameVal, } : null; @@ -38,6 +39,7 @@ private NexusModsUserEntity(NexusModsUserId userId, NexusModsUserName? name = nu { ToRoles.Add(new() { + NexusModsUserId = userId, NexusModsUser = this, TenantId = tenantVal, Role = roleVal, diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToCrashReportEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToCrashReportEntity.cs index 76231c23..feee1ccc 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToCrashReportEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToCrashReportEntity.cs @@ -6,6 +6,7 @@ public sealed record NexusModsUserToCrashReportEntity : IEntityWithTenant { public required TenantId TenantId { get; init; } + public required NexusModsUserId NexusModsUserId { get; init; } public required NexusModsUserEntity NexusModsUser { get; init; } public required CrashReportId CrashReportId { get; init; } @@ -15,5 +16,5 @@ public sealed record NexusModsUserToCrashReportEntity : IEntityWithTenant public required string Comment { get; init; } - public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsUser.NexusModsUserId, CrashReportId, Status, Comment); + public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsUserId, CrashReportId, Status, Comment); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationDiscordEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationDiscordEntity.cs index 868860aa..224891b2 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationDiscordEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationDiscordEntity.cs @@ -4,10 +4,11 @@ namespace BUTR.Site.NexusMods.Server.Models.Database; public sealed record NexusModsUserToIntegrationDiscordEntity : IEntity { + public required NexusModsUserId NexusModsUserId { get; init; } public required NexusModsUserEntity NexusModsUser { get; init; } public IntegrationDiscordTokensEntity? ToTokens { get; init; } public required string DiscordUserId { get; init; } - public override int GetHashCode() => HashCode.Combine(NexusModsUser.NexusModsUserId, DiscordUserId); + public override int GetHashCode() => HashCode.Combine(NexusModsUserId, DiscordUserId); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationGOGEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationGOGEntity.cs index 52235076..063ddf9d 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationGOGEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationGOGEntity.cs @@ -5,11 +5,12 @@ namespace BUTR.Site.NexusMods.Server.Models.Database; public sealed record NexusModsUserToIntegrationGOGEntity : IEntity { + public required NexusModsUserId NexusModsUserId { get; init; } public required NexusModsUserEntity NexusModsUser { get; init; } public IntegrationGOGTokensEntity? ToTokens { get; init; } public required string GOGUserId { get; init; } public ICollection ToOwnedTenants { get; init; } = new List(); - public override int GetHashCode() => HashCode.Combine(NexusModsUser.NexusModsUserId, GOGUserId); + public override int GetHashCode() => HashCode.Combine(NexusModsUserId, GOGUserId); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationGitHubEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationGitHubEntity.cs index 5be091a5..7dfc6eb1 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationGitHubEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationGitHubEntity.cs @@ -4,11 +4,12 @@ namespace BUTR.Site.NexusMods.Server.Models.Database; public sealed record NexusModsUserToIntegrationGitHubEntity : IEntity { + public required NexusModsUserId NexusModsUserId { get; init; } public required NexusModsUserEntity NexusModsUser { get; init; } public IntegrationGitHubTokensEntity? ToTokens { get; init; } public required string GitHubUserId { get; init; } - public override int GetHashCode() => HashCode.Combine(NexusModsUser.NexusModsUserId, GitHubUserId); + public override int GetHashCode() => HashCode.Combine(NexusModsUserId, GitHubUserId); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationSteamEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationSteamEntity.cs index d8d1cc9a..d7a35983 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationSteamEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationSteamEntity.cs @@ -5,6 +5,7 @@ namespace BUTR.Site.NexusMods.Server.Models.Database; public sealed record NexusModsUserToIntegrationSteamEntity : IEntity { + public required NexusModsUserId NexusModsUserId { get; init; } public required NexusModsUserEntity NexusModsUser { get; init; } public IntegrationSteamTokensEntity? ToTokens { get; init; } @@ -12,5 +13,5 @@ public sealed record NexusModsUserToIntegrationSteamEntity : IEntity public ICollection ToOwnedTenants { get; init; } = new List(); - public override int GetHashCode() => HashCode.Combine(NexusModsUser.NexusModsUserId, SteamUserId); + public override int GetHashCode() => HashCode.Combine(NexusModsUserId, SteamUserId); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToModuleEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToModuleEntity.cs index 7e9aef18..a8083d37 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToModuleEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToModuleEntity.cs @@ -6,11 +6,13 @@ public sealed record NexusModsUserToModuleEntity : IEntityWithTenant { public required TenantId TenantId { get; init; } + public required NexusModsUserId NexusModsUserId { get; init; } public required NexusModsUserEntity NexusModsUser { get; init; } + public required ModuleId ModuleId { get; init; } public required ModuleEntity Module { get; init; } public required NexusModsUserToModuleLinkType LinkType { get; init; } - public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsUser.NexusModsUserId, Module.ModuleId, LinkType); + public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsUserId, ModuleId, LinkType); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToNameEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToNameEntity.cs index cd617545..97cddedd 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToNameEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToNameEntity.cs @@ -4,9 +4,10 @@ namespace BUTR.Site.NexusMods.Server.Models.Database; public sealed record NexusModsUserToNameEntity : IEntity { + public required NexusModsUserId NexusModsUserId { get; init; } public required NexusModsUserEntity NexusModsUser { get; init; } public required NexusModsUserName Name { get; init; } - public override int GetHashCode() => HashCode.Combine(NexusModsUser.NexusModsUserId, Name); + public override int GetHashCode() => HashCode.Combine(NexusModsUserId, Name); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToNexusModsModEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToNexusModsModEntity.cs index 8c1adf03..11f4f654 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToNexusModsModEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToNexusModsModEntity.cs @@ -6,11 +6,13 @@ public record NexusModsUserToNexusModsModEntity : IEntityWithTenant { public required TenantId TenantId { get; init; } + public required NexusModsUserId NexusModsUserId { get; init; } public required NexusModsUserEntity NexusModsUser { get; init; } + public required NexusModsModId NexusModsModId { get; init; } public required NexusModsModEntity NexusModsMod { get; init; } public required NexusModsUserToNexusModsModLinkType LinkType { get; init; } - public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsUser.NexusModsUserId, NexusModsMod.NexusModsModId, LinkType); + public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsUserId, NexusModsModId, LinkType); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToRoleEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToRoleEntity.cs index c7930aea..27511e9a 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToRoleEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToRoleEntity.cs @@ -6,9 +6,10 @@ public sealed record NexusModsUserToRoleEntity : IEntityWithTenant { public required TenantId TenantId { get; init; } + public required NexusModsUserId NexusModsUserId { get; init; } public required NexusModsUserEntity NexusModsUser { get; init; } public required ApplicationRole Role { get; init; } - public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsUser.NexusModsUserId, Role); + public override int GetHashCode() => HashCode.Combine(TenantId, NexusModsUserId, Role); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/QuartzExecutionLogEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/QuartzExecutionLogEntity.cs index 97a7574c..a8887707 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/QuartzExecutionLogEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/QuartzExecutionLogEntity.cs @@ -17,17 +17,17 @@ public sealed record QuartzExecutionLogEntity : IEntity public required int RetryCount { get; init; } - public required bool? IsSuccess { get; set; } - public required bool? IsException { get; set; } - public required bool? IsVetoed { get; set; } + public required bool? IsSuccess { get; init; } + public required bool? IsException { get; init; } + public required bool? IsVetoed { get; init; } - public required string? ErrorMessage { get; set; } - public QuartzExecutionLogDetailEntity? ExecutionLogDetail { get; set; } - public required string? Result { get; set; } - public required string? ReturnCode { get; set; } + public required string? ErrorMessage { get; init; } + public QuartzExecutionLogDetailEntity? ExecutionLogDetail { get; init; } + public required string? Result { get; init; } + public required string? ReturnCode { get; init; } public DateTimeOffset DateAddedUtc { get; init; } = DateTimeOffset.UtcNow; - public string? MachineName { get; set; } = Environment.MachineName; + public string? MachineName { get; init; } = Environment.MachineName; public DateTimeOffset? GetFinishTimeUtc() => FireTimeUtc.Add(JobRunTime); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsCrashScoreInvolvedEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsCrashScoreInvolvedEntity.cs index 0f2b21f9..4ccd7286 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsCrashScoreInvolvedEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsCrashScoreInvolvedEntity.cs @@ -8,6 +8,7 @@ public sealed record StatisticsCrashScoreInvolvedEntity : IEntityWithTenant public required Guid StatisticsCrashScoreInvolvedId { get; init; } public required GameVersion GameVersion { get; init; } + public required ModuleId ModuleId { get; init; } public required ModuleEntity Module { get; init; } public required ModuleVersion ModuleVersion { get; init; } public required int InvolvedCount { get; init; } @@ -16,5 +17,5 @@ public sealed record StatisticsCrashScoreInvolvedEntity : IEntityWithTenant public required int RawValue { get; init; } public required double Score { get; init; } - public override int GetHashCode() => HashCode.Combine(StatisticsCrashScoreInvolvedId, GameVersion, Module.ModuleId, ModuleVersion, InvolvedCount, NotInvolvedCount, TotalCount, HashCode.Combine(RawValue, Score)); + public override int GetHashCode() => HashCode.Combine(TenantId, StatisticsCrashScoreInvolvedId, GameVersion, ModuleId, ModuleVersion, InvolvedCount, NotInvolvedCount, HashCode.Combine(TotalCount, RawValue, Score)); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsTopExceptionsTypeEntity.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsTopExceptionsTypeEntity.cs index dd4e5fc0..97592c52 100644 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsTopExceptionsTypeEntity.cs +++ b/src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsTopExceptionsTypeEntity.cs @@ -6,9 +6,10 @@ public sealed record StatisticsTopExceptionsTypeEntity : IEntityWithTenant { public required TenantId TenantId { get; init; } + public required ExceptionTypeId ExceptionTypeId { get; init; } public required ExceptionTypeEntity ExceptionType { get; init; } public required int ExceptionCount { get; init; } - public override int GetHashCode() => HashCode.Combine(ExceptionType, ExceptionCount); + public override int GetHashCode() => HashCode.Combine(TenantId, ExceptionTypeId, ExceptionCount); } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsTopExceptionsTypeResult.cs b/src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsTopExceptionsTypeResult.cs deleted file mode 100644 index 41cb6334..00000000 --- a/src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsTopExceptionsTypeResult.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace BUTR.Site.NexusMods.Server.Models.Database; - -public sealed record StatisticsTopExceptionsTypeResult -{ - public required string Type { get; init; } - public required int Count { get; init; } -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Options/NexusModsUsersOptions.cs b/src/BUTR.Site.NexusMods.Server/Options/NexusModsUsersOptions.cs new file mode 100644 index 00000000..e28a4466 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Options/NexusModsUsersOptions.cs @@ -0,0 +1,22 @@ +using Aragas.Extensions.Options.FluentValidation.Extensions; + +using FluentValidation; + +using System.Net.Http; + +namespace BUTR.Site.NexusMods.Server.Options; + +public sealed class NexusModsUsersOptionsValidator : AbstractValidator +{ + public NexusModsUsersOptionsValidator(HttpClient client) + { + RuleFor(x => x.ClientId).NotEmpty(); + RuleFor(x => x.RedirectUri).NotEmpty().IsUri(); + } +} + +public sealed record NexusModsUsersOptions +{ + public required string ClientId { get; init; } + public required string RedirectUri { get; init; } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/AutocompleteEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/AutocompleteEntityRepository.cs new file mode 100644 index 00000000..96ecae16 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/AutocompleteEntityRepository.cs @@ -0,0 +1,42 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Jobs; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface IAutocompleteEntityRepositoryRead : IRepositoryRead +{ + Task> AutocompleteStartsWithAsync(Expression> property, TParameter value, CancellationToken ct) + where TEntity : class, IEntity; +} +public interface IAutocompleteEntityRepositoryWrite : IRepositoryWrite, IAutocompleteEntityRepositoryRead; + +[ScopedService] +internal class AutocompleteEntityRepository : Repository, IAutocompleteEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery; + + public AutocompleteEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } + + public async Task> AutocompleteStartsWithAsync(Expression> property, TParameter value, CancellationToken ct) + where TEntity : class, IEntity + { + var key = AutocompleteProcessorProcessorJob.GenerateName(property); + return await _dbContext.Autocompletes + .Where(x => x.Type == key) + .Where(x => EF.Functions.ILike(x.Value, $"%{value}%")) + .Select(x => x.Value) + .OrderBy(x => x) + .ToListAsync(ct); + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportEntityRepository.cs new file mode 100644 index 00000000..3f221068 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportEntityRepository.cs @@ -0,0 +1,109 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Extensions; +using BUTR.Site.NexusMods.Server.Jobs; +using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Models.API; +using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Shared.Helpers; + +using EFCore.BulkExtensions; + +using Microsoft.EntityFrameworkCore; + +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface ICrashReportEntityRepositoryRead : IRepositoryRead +{ + Task> GetCrashReportsPaginatedAsync(NexusModsUserEntity user, PaginatedQuery query, ApplicationRole applicationRole, CancellationToken ct); +} + +public interface ICrashReportEntityRepositoryWrite : IRepositoryWrite, ICrashReportEntityRepositoryRead +{ + Task GenerateAutoCompleteForGameVersionsAsync(CancellationToken ct); +} + +[ScopedService] +internal class CrashReportEntityRepository : Repository, ICrashReportEntityRepositoryWrite +{ + private readonly ITenantContextAccessor _tenantContextAccessor; + + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.ExceptionType) + .Include(x => x.FileId) + .Include(x => x.Metadata) + .Include(x => x.ModuleInfos) + .Include(x => x.ToUsers); + + public CrashReportEntityRepository(IAppDbContextProvider appDbContextProvider, ITenantContextAccessor tenantContextAccessor) : base(appDbContextProvider.Get()) + { + _tenantContextAccessor = tenantContextAccessor; + } + + public async Task GenerateAutoCompleteForGameVersionsAsync(CancellationToken ct) + { + var tenant = _tenantContextAccessor.Current; + var key = AutocompleteProcessorProcessorJob.GenerateName(x => x.GameVersion); + + await _dbContext.Autocompletes.Where(x => x.Type == key).ExecuteDeleteAsync(ct); + + var data = await _dbContext.CrashReports.Select(x => x.GameVersion.Value).Distinct().Select(x => new AutocompleteEntity + { + AutocompleteId = default, + TenantId = tenant, + Type = key, + Value = x, + }).ToListAsync(ct); + await _dbContext.BulkInsertOrUpdateAsync(data, cancellationToken: ct); + } + + public async Task> GetCrashReportsPaginatedAsync(NexusModsUserEntity user, PaginatedQuery query, ApplicationRole applicationRole, CancellationToken ct) + { + var moduleIds = user.ToModules.Select(x => x.Module.ModuleId).ToHashSet(); + var nexusModsModIds = user.ToNexusModsMods.Select(x => x.NexusModsMod.NexusModsModId).ToHashSet(); + + IQueryable DbQueryBase(Expression> predicate) => _dbContext.CrashReports + .Include(x => x.ToUsers).ThenInclude(x => x.NexusModsUser) + .Include(x => x.ModuleInfos).ThenInclude(x => x.Module) + .Include(x => x.ModuleInfos).ThenInclude(x => x.NexusModsMod) + .Include(x => x.ModuleInfos).ThenInclude(x => x.Module) + .Include(x => x.ExceptionType) + .AsSplitQuery() + //.Where(x => x.CreatedAt > DateTimeOffset.Parse("2020-03-31 00:00:00")) + //.Where(x => (byte) x.Version > 10) + .Where(predicate) + .Select(x => new UserCrashReportModel + { + Id = x.CrashReportId, + Version = x.Version, + GameVersion = x.GameVersion, + ExceptionType = x.ExceptionType.ExceptionTypeId, + Exception = x.Exception, + CreatedAt = x.CreatedAt, + Url = x.Url, + //ModuleIds = x.ModuleInfos.Select(y => y.Module).Select(y => y.ModuleId).ToArray(), + //ModuleIdToVersion = x.ModuleInfos.Select(y => new ModuleIdToVersionView { ModuleId = y.Module.ModuleId, Version = y.Version }).ToArray(), + //TopInvolvedModuleId = x.ModuleInfos.OrderBy(y => y.InvolvedPosition).Where(z => z.IsInvolved).Select(y => y.Module).Select(y => y.ModuleId).Cast().FirstOrDefault(), + InvolvedModuleIds = x.ModuleInfos.OrderBy(y => y.InvolvedPosition).Where(z => z.IsInvolved).Select(y => y.Module).Select(y => y.ModuleId).ToArray(), + //NexusModsModIds = x.ModuleInfos.Select(y => y.NexusModsMod).Where(y => y != null).Select(y => y!.NexusModsModId).ToArray(), + Status = x.ToUsers.Where(y => y.NexusModsUser.NexusModsUserId == user.NexusModsUserId).Select(y => y.Status).FirstOrDefault(), + Comment = x.ToUsers.Where(y => y.NexusModsUser.NexusModsUserId == user.NexusModsUserId).Select(y => y.Comment).FirstOrDefault(), + }) + .WithFilter(query.Filters ?? []) + .WithSort(query.Sortings ?? []); + + var dbQuery = applicationRole == ApplicationRoles.Administrator || applicationRole == ApplicationRoles.Moderator + ? DbQueryBase(x => true) + : DbQueryBase(x => x.ToUsers.Any(y => y.NexusModsUser.NexusModsUserId == user.NexusModsUserId) || + x.ModuleInfos.Any(y => moduleIds.Contains(y.Module.ModuleId)) || + x.ModuleInfos.Where(y => y.NexusModsMod != null).Any(y => nexusModsModIds.Contains(y.NexusModsMod!.NexusModsModId))); + + return await dbQuery.PaginatedAsync(query.Page, query.PageSize, ct); + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportIgnoredFileEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportIgnoredFileEntityRepository.cs new file mode 100644 index 00000000..3dda1b5d --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportIgnoredFileEntityRepository.cs @@ -0,0 +1,18 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface ICrashReportIgnoredFileEntityRepositoryRead : IRepositoryRead; +public interface ICrashReportIgnoredFileEntityRepositoryWrite : IRepositoryWrite, ICrashReportIgnoredFileEntityRepositoryRead; + +[ScopedService] +internal class CrashReportIgnoredFileEntityRepository : Repository, ICrashReportIgnoredFileEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery; + + public CrashReportIgnoredFileEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToFileIdEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToFileIdEntityRepository.cs new file mode 100644 index 00000000..a6e97589 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToFileIdEntityRepository.cs @@ -0,0 +1,21 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface ICrashReportToFileIdEntityRepositoryRead : IRepositoryRead; +public interface ICrashReportToFileIdEntityRepositoryWrite : IRepositoryWrite, ICrashReportToFileIdEntityRepositoryRead; + +[ScopedService] +internal class CrashReportToFileIdEntityRepository : Repository, ICrashReportToFileIdEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.ToCrashReport); + + public CrashReportToFileIdEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToMetadataEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToMetadataEntityRepository.cs new file mode 100644 index 00000000..28a8a919 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToMetadataEntityRepository.cs @@ -0,0 +1,21 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface ICrashReportToMetadataEntityRepositoryRead : IRepositoryRead; +public interface ICrashReportToMetadataEntityRepositoryWrite : IRepositoryWrite, ICrashReportToMetadataEntityRepositoryRead; + +[ScopedService] +internal class CrashReportToMetadataEntityRepository : Repository, ICrashReportToMetadataEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.ToCrashReport); + + public CrashReportToMetadataEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToModuleMetadataEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToModuleMetadataEntityRepository.cs new file mode 100644 index 00000000..5e797d4b --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToModuleMetadataEntityRepository.cs @@ -0,0 +1,118 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Jobs; +using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Models.Database; + +using EFCore.BulkExtensions; + +using Microsoft.EntityFrameworkCore; + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public sealed record StatisticsCrashReport +{ + public required GameVersion GameVersion { get; init; } + public required ModuleId ModuleId { get; init; } + public required ModuleVersion ModuleVersion { get; init; } + public required int InvolvedCount { get; init; } + public required int NotInvolvedCount { get; init; } + public required int TotalCount { get; init; } + public required int Value { get; init; } + public required double CrashScore { get; init; } +} + +public interface ICrashReportToModuleMetadataEntityRepositoryRead : IRepositoryRead +{ + Task> GetAllStatisticsAsync(CancellationToken ct); +} + +public interface ICrashReportToModuleMetadataEntityRepositoryWrite : IRepositoryWrite, ICrashReportToModuleMetadataEntityRepositoryRead +{ + Task GenerateAutoCompleteForModuleIdsAsync(CancellationToken ct); +} + +[ScopedService] +internal class CrashReportToModuleMetadataEntityRepository : Repository, ICrashReportToModuleMetadataEntityRepositoryWrite +{ + private readonly ITenantContextAccessor _tenantContextAccessor; + + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.ToCrashReport) + .Include(x => x.Module) + .Include(x => x.NexusModsMod); + + public CrashReportToModuleMetadataEntityRepository(IAppDbContextProvider appDbContextProvider, ITenantContextAccessor tenantContextAccessor) : base(appDbContextProvider.Get()) + { + _tenantContextAccessor = tenantContextAccessor; + } + + public async Task> GetAllStatisticsAsync(CancellationToken ct) + { + var allModVersionsQuery = _dbContext.CrashReportModuleInfos + .GroupBy(x => new { x.Module.ModuleId, x.Version }) + .Select(x => new { x.Key.ModuleId, x.Key.Version }) + .Distinct(); + + var modCountsQuery = _dbContext.CrashReportModuleInfos + .Include(x => x.ToCrashReport!) + .GroupBy(x => new { x.ToCrashReport!.GameVersion, x.Module.ModuleId, x.Version }) + .Select(x => new { x.Key.GameVersion, x.Key.ModuleId, x.Key.Version, Count = x.Count() }) + .Distinct(); + + var involvedModCountsQuery = _dbContext.CrashReportModuleInfos + .Include(x => x.ToCrashReport!) + .Where(x => x.IsInvolved) + .GroupBy(x => new { x.ToCrashReport!.GameVersion, x.Module.ModuleId, x.Version }) + .Select(x => new { x.Key.GameVersion, x.Key.ModuleId, x.Key.Version, Count = x.Count() }) + .Distinct(); + + var notInvolvedModCountsQuery = _dbContext.CrashReportModuleInfos + .Include(x => x.ToCrashReport!) + .Where(x => !x.IsInvolved) + .GroupBy(x => new { x.ToCrashReport!.GameVersion, x.Module.ModuleId, x.Version }) + .Select(x => new { x.Key.GameVersion, x.Key.ModuleId, x.Key.Version, Count = x.Count() }) + .Distinct(); + + var query = + from allModVersios in allModVersionsQuery + join modCounts in modCountsQuery on new { allModVersios.ModuleId, allModVersios.Version } equals new { modCounts.ModuleId, modCounts.Version } + join involvedModCounts in involvedModCountsQuery on new { allModVersios.ModuleId, allModVersios.Version, modCounts.GameVersion } equals new { involvedModCounts.ModuleId, involvedModCounts.Version, involvedModCounts.GameVersion } + join notInvolvedModCounts in notInvolvedModCountsQuery on new { allModVersios.ModuleId, allModVersios.Version, modCounts.GameVersion } equals new { notInvolvedModCounts.ModuleId, notInvolvedModCounts.Version, notInvolvedModCounts.GameVersion } + select new StatisticsCrashReport + { + GameVersion = modCounts.GameVersion, + ModuleId = allModVersios.ModuleId, + ModuleVersion = allModVersios.Version, + InvolvedCount = involvedModCounts.Count, + NotInvolvedCount = notInvolvedModCounts.Count, + TotalCount = modCounts.Count, + Value = involvedModCounts.Count, + CrashScore = (double) involvedModCounts.Count / (double) modCounts.Count + }; + + return await query.ToListAsync(ct); + } + + public async Task GenerateAutoCompleteForModuleIdsAsync(CancellationToken ct) + { + var tenant = _tenantContextAccessor.Current; + var key = AutocompleteProcessorProcessorJob.GenerateName(x => x.Module.ModuleId); + + await _dbContext.Autocompletes.Where(x => x.Type == key).ExecuteDeleteAsync(ct); + + var data = await _dbContext.CrashReportModuleInfos.Select(y => y.Module.ModuleId.Value).Distinct().Select(x => new AutocompleteEntity + { + AutocompleteId = default, + TenantId = tenant, + Type = key, + Value = x, + }).ToListAsync(ct); + await _dbContext.BulkInsertOrUpdateAsync(data, cancellationToken: ct); + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/ExceptionTypeRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/ExceptionTypeRepository.cs new file mode 100644 index 00000000..c19d7117 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/ExceptionTypeRepository.cs @@ -0,0 +1,23 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface IExceptionTypeRepositoryRead : IRepositoryRead; +public interface IExceptionTypeRepositoryWrite : IRepositoryWrite, IExceptionTypeRepositoryRead; + +[ScopedService] +internal class ExceptionTypeRepository : Repository, IExceptionTypeRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + //.Include(x => x.ToCrashReports) + //.Include(x => x.ToTopExceptionsTypes) + ; + + public ExceptionTypeRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IRepository.cs new file mode 100644 index 00000000..29c56a78 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/IRepository.cs @@ -0,0 +1,178 @@ +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Extensions; +using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Models.API; +using BUTR.Site.NexusMods.Server.Models.Database; + +using EFCore.BulkExtensions; + +using Microsoft.EntityFrameworkCore; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +/// +/// Marker interface for repositories. +/// +public interface IRepository; + +public interface IRepositoryRead : IRepository where TEntity : class, IEntity +{ + Task FirstOrDefaultAsync( + Expression>? filter, + Func, IOrderedQueryable>? orderBy, + CancellationToken ct); + Task FirstOrDefaultAsync( + Expression>? filter, + Func, IOrderedQueryable>? orderBy, + Expression> projection, + CancellationToken ct); + + Task LastOrDefaultAsync( + Expression>? filter, + Func, IOrderedQueryable>? orderBy, + CancellationToken ct); + Task LastOrDefaultAsync( + Expression>? filter, + Func, IOrderedQueryable>? orderBy, + Expression> projection, + CancellationToken ct); + + Task> GetAllAsync( + Expression>? filter, + Func, IOrderedQueryable>? orderBy, + CancellationToken ct); + Task> GetAllAsync( + Expression>? filter, + Func, IOrderedQueryable>? orderBy, + Expression> projection, + CancellationToken ct); + + Task> PaginatedAsync( + PaginatedQuery query, + uint maxPageSize = 20, + Sorting? defaultSorting = default, + CancellationToken ct = default); + + Task> PaginatedAsync( + Expression> projection, + PaginatedQuery query, + uint maxPageSize = 20, + Sorting? defaultSorting = default, + CancellationToken ct = default) where TProjection : class; +} + +public interface IRepositoryWrite : IRepositoryRead where TEntity : class, IEntity +{ + void Add(TEntity entity); + void AddRange(IEnumerable entities); + + void Update(TEntity originalEntity, TEntity currentEntity); + + void Upsert(TEntity entity); + void UpsertRange(IEnumerable entities); + + void Remove(TEntity entity); + void RemoveRange(IEnumerable entities); + + int Remove(Expression> filter); +} + +internal abstract class Repository : IRepositoryWrite where TEntity : class, IEntity +{ + protected readonly BaseAppDbContext _dbContext; + + protected Repository(BaseAppDbContext dbContext) + { + _dbContext = dbContext; + } + + protected virtual IQueryable InternalQuery => _dbContext.Set(); + + public async Task FirstOrDefaultAsync(Expression>? filter, Func, IOrderedQueryable>? orderBy, CancellationToken ct) => await InternalQuery + .WhereIf(filter != null, filter!) + .OrderByIf(orderBy != null, orderBy!) + .FirstOrDefaultAsync(ct); + + public async Task FirstOrDefaultAsync(Expression>? filter, Func, IOrderedQueryable>? orderBy, Expression> projection, CancellationToken ct) => await InternalQuery + .WhereIf(filter != null, filter!) + .OrderByIf(orderBy != null, orderBy!) + .Select(projection) + .FirstOrDefaultAsync(ct); + + public async Task LastOrDefaultAsync(Expression>? filter, Func, IOrderedQueryable>? orderBy, CancellationToken ct) => await InternalQuery + .WhereIf(filter != null, filter!) + .OrderByIf(orderBy != null, orderBy!) + .LastOrDefaultAsync(ct); + + public async Task LastOrDefaultAsync(Expression>? filter, Func, IOrderedQueryable>? orderBy, Expression> projection, CancellationToken ct) => await InternalQuery + .WhereIf(filter != null, filter!) + .OrderByIf(orderBy != null, orderBy!) + .Select(projection) + .LastOrDefaultAsync(ct); + + public virtual async Task> GetAllAsync(Expression>? filter, Func, IOrderedQueryable>? orderBy, CancellationToken ct = default) => await InternalQuery + .WhereIf(filter != null, filter!) + .OrderByIf(orderBy != null, orderBy!) + .ToListAsync(ct); + + public async Task> GetAllAsync(Expression>? filter, Func, IOrderedQueryable>? orderBy, Expression> projection, CancellationToken ct) => await InternalQuery + .WhereIf(filter != null, filter!) + .OrderByIf(orderBy != null, orderBy!) + .Select(projection) + .ToListAsync(ct); + + public Task> PaginatedAsync(PaginatedQuery query, uint maxPageSize = 20, Sorting? defaultSorting = default, CancellationToken ct = default) => InternalQuery + .PaginatedAsync(query, maxPageSize, defaultSorting, ct); + + public Task> PaginatedAsync(Expression> projection, PaginatedQuery query, uint maxPageSize = 20, Sorting? defaultSorting = default, CancellationToken ct = default) + where TProjection : class => InternalQuery + .Select(projection) + .PaginatedAsync(query, maxPageSize, defaultSorting, ct); + + + public virtual void Add(TEntity entity) => _dbContext.Set() + .Add(entity); + + public virtual void AddRange(IEnumerable entities) => _dbContext.Set() + .AddRange(entities); + + public void Update(TEntity originalEntity, TEntity currentEntity) + { + _dbContext.UpdateProperties(originalEntity, currentEntity); + } + + public virtual void Upsert(TEntity entity) + { + if (_dbContext is AppDbContextWrite appDbContextWrite) + { + appDbContextWrite.AddFutureAction(dbContext => dbContext + .BulkInsertOrUpdateAsync([entity], o => { o.IncludeGraph = false; })); + } + } + + public virtual void UpsertRange(IEnumerable entities) + { + if (_dbContext is AppDbContextWrite appDbContextWrite) + { + appDbContextWrite.AddFutureAction(dbContext => dbContext + .BulkInsertOrUpdateAsync(entities, o => { o.IncludeGraph = false; })); + } + } + + public virtual void Remove(TEntity entity) => _dbContext.Set() + .Remove(entity); + + public virtual void RemoveRange(IEnumerable entities) => _dbContext.Set() + .RemoveRange(entities); + + public int Remove(Expression> filter) => _dbContext.Set() + .Where(filter) + .ExecuteDelete(); +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfRead.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfRead.cs new file mode 100644 index 00000000..41fa8d35 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfRead.cs @@ -0,0 +1,169 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +using System; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface IUnitOfRead : IDisposable, IAsyncDisposable +{ + IAutocompleteEntityRepositoryRead Autocompletes { get; } + + IQuartzExecutionLogEntityRepositoryRead QuartzExecutionLogs { get; } + + IExceptionTypeRepositoryRead ExceptionTypes { get; } + + ICrashReportEntityRepositoryRead CrashReports { get; } + ICrashReportToMetadataEntityRepositoryRead CrashReportToMetadatas { get; } + ICrashReportToModuleMetadataEntityRepositoryRead CrashReportModuleInfos { get; } + ICrashReportToFileIdEntityRepositoryRead CrashReportToFileIds { get; } + ICrashReportIgnoredFileEntityRepositoryRead CrashReportIgnoredFileIds { get; } + + IStatisticsTopExceptionsTypeEntityRepositoryRead StatisticsTopExceptionsTypes { get; } + IStatisticsCrashScoreInvolvedEntityRepositoryRead StatisticsCrashScoreInvolveds { get; } + + INexusModsArticleEntityRepositoryRead NexusModsArticles { get; } + + INexusModsModToModuleEntityRepositoryRead NexusModsModModules { get; } + INexusModsModToNameEntityRepositoryRead NexusModsModName { get; } + + INexusModsModToModuleInfoHistoryEntityRepositoryRead NexusModsModToModuleInfoHistory { get; } + INexusModsModToFileUpdateEntityRepositoryRead NexusModsModToFileUpdates { get; } + + INexusModsUserRepositoryRead NexusModsUsers { get; } + INexusModsUserToNameEntityRepositoryRead NexusModsUserToName { get; } + INexusModsUserToCrashReportEntityRepositoryRead NexusModsUserToCrashReports { get; } + INexusModsUserToNexusModsModEntityRepositoryRead NexusModsUserToNexusModsMods { get; } + INexusModsUserToModuleEntityRepositoryRead NexusModsUserToModules { get; } + + + INexusModsUserToIntegrationGitHubEntityRepositoryRead NexusModsUserToGitHub { get; } + INexusModsUserToIntegrationDiscordEntityRepositoryRead NexusModsUserToDiscord { get; } + INexusModsUserToIntegrationGOGEntityRepositoryRead NexusModsUserToGOG { get; } + INexusModsUserToIntegrationSteamEntityRepositoryRead NexusModsUserToSteam { get; } + + IIntegrationGitHubTokensEntityRepositoryRead IntegrationGitHubTokens { get; } + IIntegrationDiscordTokensEntityRepositoryRead IntegrationDiscordTokens { get; } + IIntegrationGOGTokensEntityRepositoryRead IntegrationGOGTokens { get; } + IIntegrationGOGToOwnedTenantEntityRepositoryRead IntegrationGOGToOwnedTenants { get; } + IIntegrationSteamTokensEntityRepositoryRead IntegrationSteamTokens { get; } + IIntegrationSteamToOwnedTenantEntityRepositoryRead IntegrationSteamToOwnedTenants { get; } +} + +[TransientService] +internal class UnitOfRead : IUnitOfRead +{ + private readonly IServiceScope _serviceScope; + private readonly AppDbContextRead _dbContext; + + public IAutocompleteEntityRepositoryRead Autocompletes { get; } + + public IQuartzExecutionLogEntityRepositoryRead QuartzExecutionLogs { get; } + + public IExceptionTypeRepositoryRead ExceptionTypes { get; } + + public ICrashReportEntityRepositoryRead CrashReports { get; } + public ICrashReportToMetadataEntityRepositoryRead CrashReportToMetadatas { get; } + public ICrashReportToModuleMetadataEntityRepositoryRead CrashReportModuleInfos { get; } + public ICrashReportToFileIdEntityRepositoryRead CrashReportToFileIds { get; } + public ICrashReportIgnoredFileEntityRepositoryRead CrashReportIgnoredFileIds { get; } + + public IStatisticsTopExceptionsTypeEntityRepositoryRead StatisticsTopExceptionsTypes { get; } + public IStatisticsCrashScoreInvolvedEntityRepositoryRead StatisticsCrashScoreInvolveds { get; } + + public INexusModsArticleEntityRepositoryRead NexusModsArticles { get; } + + public INexusModsModToModuleEntityRepositoryRead NexusModsModModules { get; } + public INexusModsModToNameEntityRepositoryRead NexusModsModName { get; } + public INexusModsModToModuleInfoHistoryEntityRepositoryRead NexusModsModToModuleInfoHistory { get; } + public INexusModsModToFileUpdateEntityRepositoryRead NexusModsModToFileUpdates { get; } + + public INexusModsUserRepositoryRead NexusModsUsers { get; } + public INexusModsUserToNameEntityRepositoryRead NexusModsUserToName { get; } + public INexusModsUserToCrashReportEntityRepositoryRead NexusModsUserToCrashReports { get; } + public INexusModsUserToNexusModsModEntityRepositoryRead NexusModsUserToNexusModsMods { get; } + public INexusModsUserToModuleEntityRepositoryRead NexusModsUserToModules { get; } + + public INexusModsUserToIntegrationGitHubEntityRepositoryRead NexusModsUserToGitHub { get; } + public INexusModsUserToIntegrationDiscordEntityRepositoryRead NexusModsUserToDiscord { get; } + public INexusModsUserToIntegrationGOGEntityRepositoryRead NexusModsUserToGOG { get; } + public INexusModsUserToIntegrationSteamEntityRepositoryRead NexusModsUserToSteam { get; } + + public IIntegrationGitHubTokensEntityRepositoryRead IntegrationGitHubTokens { get; } + public IIntegrationDiscordTokensEntityRepositoryRead IntegrationDiscordTokens { get; } + public IIntegrationGOGTokensEntityRepositoryRead IntegrationGOGTokens { get; } + public IIntegrationGOGToOwnedTenantEntityRepositoryRead IntegrationGOGToOwnedTenants { get; } + public IIntegrationSteamTokensEntityRepositoryRead IntegrationSteamTokens { get; } + public IIntegrationSteamToOwnedTenantEntityRepositoryRead IntegrationSteamToOwnedTenants { get; } + + public UnitOfRead(IServiceScopeFactory serviceScopeFactory) + { + _serviceScope = serviceScopeFactory.CreateScope(); + var dbContextFactory = _serviceScope.ServiceProvider.GetRequiredService>(); + _dbContext = dbContextFactory.CreateDbContext(); + + var dbContextProvider = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider); + dbContextProvider.Set(_dbContext); + + Autocompletes = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + + QuartzExecutionLogs = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + + ExceptionTypes = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + + CrashReports = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + CrashReportToMetadatas = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + CrashReportModuleInfos = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + CrashReportToFileIds = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + CrashReportIgnoredFileIds = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + + StatisticsTopExceptionsTypes = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + StatisticsCrashScoreInvolveds = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + + NexusModsArticles = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + + NexusModsModModules = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + NexusModsModName = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + NexusModsModToModuleInfoHistory = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + NexusModsModToFileUpdates = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + + NexusModsUsers = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + NexusModsUserToName = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + NexusModsUserToCrashReports = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + NexusModsUserToNexusModsMods = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + NexusModsUserToModules = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + + NexusModsUserToGitHub = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + NexusModsUserToDiscord = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + NexusModsUserToGOG = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + NexusModsUserToSteam = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + + IntegrationGitHubTokens = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + IntegrationDiscordTokens = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + IntegrationGOGTokens = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + IntegrationGOGToOwnedTenants = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + IntegrationSteamTokens = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + IntegrationSteamToOwnedTenants = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, dbContextProvider); + } + + public void Dispose() + { + _dbContext.ChangeTracker.Clear(); + + _serviceScope.Dispose(); + } + + public async ValueTask DisposeAsync() + { + _dbContext.ChangeTracker.Clear(); + + if (_serviceScope is IAsyncDisposable serviceScopeAsyncDisposable) + await serviceScopeAsyncDisposable.DisposeAsync(); + else + _serviceScope.Dispose(); + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfWorkFactory.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfWorkFactory.cs new file mode 100644 index 00000000..a24e691a --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfWorkFactory.cs @@ -0,0 +1,59 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models; + +using Microsoft.Extensions.DependencyInjection; + +using System; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface IUnitOfWorkFactory +{ + IUnitOfRead CreateUnitOfRead(); + IUnitOfRead CreateUnitOfRead(TenantId tenant); + + IUnitOfWrite CreateUnitOfWrite(); + IUnitOfWrite CreateUnitOfWrite(TenantId tenant); +} + +[ScopedService] +internal class UnitOfWorkFactory : IUnitOfWorkFactory +{ + private readonly IServiceProvider _serviceProvider; + + public UnitOfWorkFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + + public IUnitOfRead CreateUnitOfRead() + { + var tenantContextAccessor = _serviceProvider.GetRequiredService(); + if (tenantContextAccessor.Current == TenantId.Error) + throw new InvalidOperationException("Tenant context is not set."); + + return _serviceProvider.GetRequiredService(); + } + public IUnitOfRead CreateUnitOfRead(TenantId tenant) + { + var tenantContextAccessor = _serviceProvider.GetRequiredService(); + tenantContextAccessor.Current = tenant; + + return _serviceProvider.GetRequiredService(); + } + + public IUnitOfWrite CreateUnitOfWrite() + { + var tenantContextAccessor = _serviceProvider.GetRequiredService(); + if (tenantContextAccessor.Current == TenantId.Error) + throw new InvalidOperationException("Tenant context is not set."); + + return _serviceProvider.GetRequiredService(); + } + + public IUnitOfWrite CreateUnitOfWrite(TenantId tenant) + { + var tenantContextAccessor = _serviceProvider.GetRequiredService(); + tenantContextAccessor.Current = tenant; + + return _serviceProvider.GetRequiredService(); + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfWrite.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfWrite.cs new file mode 100644 index 00000000..8af011bf --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfWrite.cs @@ -0,0 +1,188 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface IUnitOfWrite : IDisposable, IAsyncDisposable +{ + UpsertEntityFactory UpsertEntityFactory { get; } + + IAutocompleteEntityRepositoryWrite Autocompletes { get; } + + IQuartzExecutionLogEntityRepositoryWrite QuartzExecutionLogs { get; } + + IExceptionTypeRepositoryWrite ExceptionTypes { get; } + + ICrashReportEntityRepositoryWrite CrashReports { get; } + ICrashReportToMetadataEntityRepositoryWrite CrashReportToMetadatas { get; } + ICrashReportToModuleMetadataEntityRepositoryWrite CrashReportModuleInfos { get; } + ICrashReportToFileIdEntityRepositoryWrite CrashReportToFileIds { get; } + ICrashReportIgnoredFileEntityRepositoryWrite CrashReportIgnoredFileIds { get; } + + IStatisticsTopExceptionsTypeEntityRepositoryWrite StatisticsTopExceptionsTypes { get; } + IStatisticsCrashScoreInvolvedEntityRepositoryWrite StatisticsCrashScoreInvolveds { get; } + + INexusModsArticleEntityRepositoryWrite NexusModsArticles { get; } + + INexusModsModToModuleEntityRepositoryWrite NexusModsModModules { get; } + INexusModsModToNameEntityRepositoryWrite NexusModsModName { get; } + INexusModsModToModuleInfoHistoryEntityRepositoryWrite NexusModsModToModuleInfoHistory { get; } + INexusModsModToFileUpdateEntityRepositoryWrite NexusModsModToFileUpdates { get; } + + INexusModsUserRepositoryWrite NexusModsUsers { get; } + INexusModsUserToNameEntityRepositoryWrite NexusModsUserToName { get; } + INexusModsUserToCrashReportEntityRepositoryWrite NexusModsUserToCrashReports { get; } + INexusModsUserToNexusModsModEntityRepositoryWrite NexusModsUserToNexusModsMods { get; } + INexusModsUserToModuleEntityRepositoryWrite NexusModsUserToModules { get; } + + INexusModsUserToIntegrationGitHubEntityRepositoryWrite NexusModsUserToGitHub { get; } + INexusModsUserToIntegrationDiscordEntityRepositoryWrite NexusModsUserToDiscord { get; } + INexusModsUserToIntegrationGOGEntityRepositoryWrite NexusModsUserToGOG { get; } + INexusModsUserToIntegrationSteamEntityRepositoryWrite NexusModsUserToSteam { get; } + + IIntegrationGitHubTokensEntityRepositoryWrite IntegrationGitHubTokens { get; } + IIntegrationDiscordTokensEntityRepositoryWrite IntegrationDiscordTokens { get; } + IIntegrationGOGTokensEntityRepositoryWrite IntegrationGOGTokens { get; } + IIntegrationGOGToOwnedTenantEntityRepositoryWrite IntegrationGOGToOwnedTenants { get; } + IIntegrationSteamTokensEntityRepositoryWrite IntegrationSteamTokens { get; } + IIntegrationSteamToOwnedTenantEntityRepositoryWrite IntegrationSteamToOwnedTenants { get; } + + Task SaveChangesAsync(CancellationToken ct); +} + +[TransientService] +internal class UnitOfWrite : IUnitOfWrite +{ + private readonly AppDbContextWrite _dbContext; + + private IDbContextTransaction _dbContextTransaction; + + + public UpsertEntityFactory UpsertEntityFactory { get; } + + public IAutocompleteEntityRepositoryWrite Autocompletes { get; } + + public IQuartzExecutionLogEntityRepositoryWrite QuartzExecutionLogs { get; } + + public IExceptionTypeRepositoryWrite ExceptionTypes { get; } + + public ICrashReportEntityRepositoryWrite CrashReports { get; } + public ICrashReportToMetadataEntityRepositoryWrite CrashReportToMetadatas { get; } + public ICrashReportToModuleMetadataEntityRepositoryWrite CrashReportModuleInfos { get; } + public ICrashReportToFileIdEntityRepositoryWrite CrashReportToFileIds { get; } + public ICrashReportIgnoredFileEntityRepositoryWrite CrashReportIgnoredFileIds { get; } + + public IStatisticsTopExceptionsTypeEntityRepositoryWrite StatisticsTopExceptionsTypes { get; } + public IStatisticsCrashScoreInvolvedEntityRepositoryWrite StatisticsCrashScoreInvolveds { get; } + + public INexusModsArticleEntityRepositoryWrite NexusModsArticles { get; } + + public INexusModsModToModuleEntityRepositoryWrite NexusModsModModules { get; } + public INexusModsModToNameEntityRepositoryWrite NexusModsModName { get; } + public INexusModsModToModuleInfoHistoryEntityRepositoryWrite NexusModsModToModuleInfoHistory { get; } + public INexusModsModToFileUpdateEntityRepositoryWrite NexusModsModToFileUpdates { get; } + + public INexusModsUserRepositoryWrite NexusModsUsers { get; } + public INexusModsUserToNameEntityRepositoryWrite NexusModsUserToName { get; } + public INexusModsUserToCrashReportEntityRepositoryWrite NexusModsUserToCrashReports { get; } + public INexusModsUserToNexusModsModEntityRepositoryWrite NexusModsUserToNexusModsMods { get; } + public INexusModsUserToModuleEntityRepositoryWrite NexusModsUserToModules { get; } + + public INexusModsUserToIntegrationGitHubEntityRepositoryWrite NexusModsUserToGitHub { get; } + public INexusModsUserToIntegrationDiscordEntityRepositoryWrite NexusModsUserToDiscord { get; } + public INexusModsUserToIntegrationGOGEntityRepositoryWrite NexusModsUserToGOG { get; } + public INexusModsUserToIntegrationSteamEntityRepositoryWrite NexusModsUserToSteam { get; } + + public IIntegrationGitHubTokensEntityRepositoryWrite IntegrationGitHubTokens { get; } + public IIntegrationDiscordTokensEntityRepositoryWrite IntegrationDiscordTokens { get; } + public IIntegrationGOGTokensEntityRepositoryWrite IntegrationGOGTokens { get; } + public IIntegrationGOGToOwnedTenantEntityRepositoryWrite IntegrationGOGToOwnedTenants { get; } + public IIntegrationSteamTokensEntityRepositoryWrite IntegrationSteamTokens { get; } + public IIntegrationSteamToOwnedTenantEntityRepositoryWrite IntegrationSteamToOwnedTenants { get; } + + public UnitOfWrite(IServiceProvider serviceProvider) + { + var dbContextFactory = serviceProvider.GetRequiredService>(); + _dbContext = dbContextFactory.CreateDbContext(); + + var dbContextProvider = (IAppDbContextProvider) ActivatorUtilities.CreateInstance(serviceProvider); + dbContextProvider.Set(_dbContext); + + _dbContextTransaction = _dbContext.Database.BeginTransaction(); + + UpsertEntityFactory = _dbContext.GetEntityFactory(); + + Autocompletes = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + + QuartzExecutionLogs = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + + ExceptionTypes = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + + CrashReports = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + CrashReportToMetadatas = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + CrashReportModuleInfos = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + CrashReportToFileIds = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + CrashReportIgnoredFileIds = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + + StatisticsTopExceptionsTypes = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + StatisticsCrashScoreInvolveds = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + + NexusModsArticles = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + + NexusModsModModules = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + NexusModsModName = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + NexusModsModToModuleInfoHistory = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + NexusModsModToFileUpdates = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + + NexusModsUsers = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + NexusModsUserToName = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + NexusModsUserToCrashReports = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + NexusModsUserToNexusModsMods = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + NexusModsUserToModules = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + + NexusModsUserToGitHub = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + NexusModsUserToDiscord = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + NexusModsUserToGOG = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + NexusModsUserToSteam = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + + IntegrationGitHubTokens = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + IntegrationDiscordTokens = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + IntegrationGOGTokens = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + IntegrationGOGToOwnedTenants = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + IntegrationSteamTokens = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + IntegrationSteamToOwnedTenants = ActivatorUtilities.CreateInstance(serviceProvider, dbContextProvider); + } + + public async Task SaveChangesAsync(CancellationToken ct) + { + try + { + await _dbContext.SaveAsync(ct); + await _dbContextTransaction.CommitAsync(ct); + _dbContextTransaction = _dbContext.Database.BeginTransaction(); + } + catch (Exception e) + { + throw; + //return Result.Fail($"Error message: {e.Message}, Inner Exception: {e.InnerException}, StackTrace: {e.StackTrace}"); + } + } + + public void Dispose() + { + _dbContext.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _dbContext.DisposeAsync(); + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationDiscordTokensEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationDiscordTokensEntityRepository.cs new file mode 100644 index 00000000..7e29fc3d --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationDiscordTokensEntityRepository.cs @@ -0,0 +1,22 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface IIntegrationDiscordTokensEntityRepositoryRead : IRepositoryRead; +public interface IIntegrationDiscordTokensEntityRepositoryWrite : IRepositoryWrite, IIntegrationDiscordTokensEntityRepositoryRead; + +[ScopedService] +internal class IntegrationDiscordTokensEntityRepository : Repository, IIntegrationDiscordTokensEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsUser).ThenInclude(x => x.Name) + .Include(x => x.UserToDiscord); + + public IntegrationDiscordTokensEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGOGToOwnedTenantEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGOGToOwnedTenantEntityRepository.cs new file mode 100644 index 00000000..78fb19bb --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGOGToOwnedTenantEntityRepository.cs @@ -0,0 +1,18 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface IIntegrationGOGToOwnedTenantEntityRepositoryRead : IRepositoryRead; +public interface IIntegrationGOGToOwnedTenantEntityRepositoryWrite : IRepositoryWrite, IIntegrationGOGToOwnedTenantEntityRepositoryRead; + +[ScopedService] +internal class IntegrationGOGToOwnedTenantEntityRepository : Repository, IIntegrationGOGToOwnedTenantEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery; + + public IntegrationGOGToOwnedTenantEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGOGTokensEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGOGTokensEntityRepository.cs new file mode 100644 index 00000000..58887c93 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGOGTokensEntityRepository.cs @@ -0,0 +1,22 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface IIntegrationGOGTokensEntityRepositoryRead : IRepositoryRead; +public interface IIntegrationGOGTokensEntityRepositoryWrite : IRepositoryWrite, IIntegrationGOGTokensEntityRepositoryRead; + +[ScopedService] +internal class IntegrationGOGTokensEntityRepository : Repository, IIntegrationGOGTokensEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsUser).ThenInclude(x => x.Name) + .Include(x => x.UserToGOG); + + public IntegrationGOGTokensEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGitHubTokensEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGitHubTokensEntityRepository.cs new file mode 100644 index 00000000..7ebdb6b8 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGitHubTokensEntityRepository.cs @@ -0,0 +1,22 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface IIntegrationGitHubTokensEntityRepositoryRead : IRepositoryRead; +public interface IIntegrationGitHubTokensEntityRepositoryWrite : IRepositoryWrite, IIntegrationGitHubTokensEntityRepositoryRead; + +[ScopedService] +internal class IntegrationGitHubTokensEntityRepository : Repository, IIntegrationGitHubTokensEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsUser).ThenInclude(x => x.Name) + .Include(x => x.UserToGitHub); + + public IntegrationGitHubTokensEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationSteamToOwnedTenantEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationSteamToOwnedTenantEntityRepository.cs new file mode 100644 index 00000000..b9755460 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationSteamToOwnedTenantEntityRepository.cs @@ -0,0 +1,18 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface IIntegrationSteamToOwnedTenantEntityRepositoryRead : IRepositoryRead; +public interface IIntegrationSteamToOwnedTenantEntityRepositoryWrite : IRepositoryWrite, IIntegrationSteamToOwnedTenantEntityRepositoryRead; + +[ScopedService] +internal class IntegrationSteamToOwnedTenantEntityRepository : Repository, IIntegrationSteamToOwnedTenantEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery; + + public IntegrationSteamToOwnedTenantEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationSteamTokensEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationSteamTokensEntityRepository.cs new file mode 100644 index 00000000..bf960da3 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationSteamTokensEntityRepository.cs @@ -0,0 +1,21 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface IIntegrationSteamTokensEntityRepositoryRead : IRepositoryRead; +public interface IIntegrationSteamTokensEntityRepositoryWrite : IRepositoryWrite, IIntegrationSteamTokensEntityRepositoryRead; + +[ScopedService] +internal class IntegrationSteamTokensEntityRepository : Repository, IIntegrationSteamTokensEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsUser).ThenInclude(x => x.Name); + + public IntegrationSteamTokensEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsArticleEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsArticleEntityRepository.cs new file mode 100644 index 00000000..b3245b3b --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsArticleEntityRepository.cs @@ -0,0 +1,64 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Jobs; +using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Models.Database; + +using EFCore.BulkExtensions; + +using Microsoft.EntityFrameworkCore; + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface INexusModsArticleEntityRepositoryRead : IRepositoryRead +{ + Task> GetAllModuleIdsAsync(string authorName, CancellationToken ct); +} + +public interface INexusModsArticleEntityRepositoryWrite : IRepositoryWrite, INexusModsArticleEntityRepositoryRead +{ + Task GenerateAutoCompleteForAuthorNameAsync(CancellationToken ct); +} + +[ScopedService] +internal class NexusModsArticleEntityRepository : Repository, INexusModsArticleEntityRepositoryWrite +{ + private readonly ITenantContextAccessor _tenantContextAccessor; + + protected override IQueryable InternalQuery => base.InternalQuery; + + public NexusModsArticleEntityRepository(IAppDbContextProvider appDbContextProvider, ITenantContextAccessor tenantContextAccessor) : base(appDbContextProvider.Get()) + { + _tenantContextAccessor = tenantContextAccessor; + } + + public async Task> GetAllModuleIdsAsync(string authorName, CancellationToken ct) => await _dbContext.NexusModsArticles + .Select(x => x.NexusModsUser) + .Select(x => x.Name!) + .Where(x => EF.Functions.ILike(x.Name.Value, $"{authorName}%")) + .Select(x => x.Name.Value) + .Distinct() + .ToListAsync(ct); + + public async Task GenerateAutoCompleteForAuthorNameAsync(CancellationToken ct) + { + var tenant = _tenantContextAccessor.Current; + var key = AutocompleteProcessorProcessorJob.GenerateName(x => x.NexusModsUser.Name!.Name); + + await _dbContext.Autocompletes.Where(x => x.Type == key).ExecuteDeleteAsync(ct); + + var data = await _dbContext.NexusModsArticles.Select(y => y.NexusModsUser.Name!.Name.Value).Distinct().Select(x => new AutocompleteEntity + { + AutocompleteId = default, + TenantId = tenant, + Type = key, + Value = x, + }).ToListAsync(ct); + await _dbContext.BulkInsertOrUpdateAsync(data, cancellationToken: ct); + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsModToFileUpdateEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsModToFileUpdateEntityRepository.cs new file mode 100644 index 00000000..628ac849 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsModToFileUpdateEntityRepository.cs @@ -0,0 +1,21 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface INexusModsModToFileUpdateEntityRepositoryRead : IRepositoryRead; +public interface INexusModsModToFileUpdateEntityRepositoryWrite : IRepositoryWrite, INexusModsModToFileUpdateEntityRepositoryRead; + +[ScopedService] +internal class NexusModsModToFileUpdateEntityRepository : Repository, INexusModsModToFileUpdateEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsMod).ThenInclude(x => x.Name); + + public NexusModsModToFileUpdateEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsModToModuleEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsModToModuleEntityRepository.cs new file mode 100644 index 00000000..06022988 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsModToModuleEntityRepository.cs @@ -0,0 +1,85 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Extensions; +using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Models.API; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public sealed record LinkedByStaffNexusModsModModel +{ + public NexusModsModId NexusModsModId { get; init; } + public DateTimeOffset LastCheckedDate { get; init; } +} + +public sealed record LinkedByStaffModuleNexusModsModsModel +{ + public required ModuleId ModuleId { get; init; } + public required LinkedByStaffNexusModsModModel[] NexusModsMods { get; init; } +} + +public sealed record LinkedByExposureModuleModel +{ + public required ModuleId ModuleId { get; init; } + public required DateTimeOffset LastCheckedDate { get; init; } +} + +public sealed record LinkedByExposureNexusModsModModelsModel +{ + public required NexusModsModId NexusModsModId { get; init; } + public required LinkedByExposureModuleModel[] Modules { get; init; } +} + +public interface INexusModsModToModuleEntityRepositoryRead : IRepositoryRead +{ + Task> GetByStaffPaginatedAsync(PaginatedQuery query, CancellationToken ct); + + Task> GetExposedPaginatedAsync(PaginatedQuery query, CancellationToken ct); +} +public interface INexusModsModToModuleEntityRepositoryWrite : IRepositoryWrite, INexusModsModToModuleEntityRepositoryRead; + +[ScopedService] +internal class NexusModsModToModuleEntityRepository : Repository, INexusModsModToModuleEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsMod).ThenInclude(x => x.Name) + .Include(x => x.Module); + + public NexusModsModToModuleEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } + + public async Task> GetByStaffPaginatedAsync(PaginatedQuery query, CancellationToken ct) => await _dbContext.NexusModsModModules + .Where(x => x.LinkType == NexusModsModToModuleLinkType.ByStaff) + .GroupBy(x => x.Module.ModuleId) + .Select(x => new LinkedByStaffModuleNexusModsModsModel + { + ModuleId = x.Key, + NexusModsMods = x.Select(y => new LinkedByStaffNexusModsModModel + { + NexusModsModId = y.NexusModsMod.NexusModsModId, + LastCheckedDate = y.LastUpdateDate.ToUniversalTime(), + }).ToArray() + }) + .PaginatedAsync(query, 100, new() { Property = nameof(LinkedByStaffModuleNexusModsModsModel.ModuleId), Type = SortingType.Ascending }, ct); + + public async Task> GetExposedPaginatedAsync(PaginatedQuery query, CancellationToken ct) => await _dbContext.NexusModsModModules + .Where(x => x.LinkType == NexusModsModToModuleLinkType.ByUnverifiedFileExposure) + .GroupBy(x => x.NexusModsMod.NexusModsModId) + .Select(x => new LinkedByExposureNexusModsModModelsModel + { + NexusModsModId = x.Key, + Modules = x.Select(y => new LinkedByExposureModuleModel + { + ModuleId = y.Module.ModuleId, + LastCheckedDate = y.LastUpdateDate.ToUniversalTime(), + }).ToArray() + }) + .PaginatedAsync(query, 100, new() { Property = nameof(LinkedByExposureNexusModsModModelsModel.NexusModsModId), Type = SortingType.Ascending }, ct); +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsModToModuleInfoHistoryEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsModToModuleInfoHistoryEntityRepository.cs new file mode 100644 index 00000000..bf5c2929 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsModToModuleInfoHistoryEntityRepository.cs @@ -0,0 +1,22 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface INexusModsModToModuleInfoHistoryEntityRepositoryRead : IRepositoryRead; +public interface INexusModsModToModuleInfoHistoryEntityRepositoryWrite : IRepositoryWrite, INexusModsModToModuleInfoHistoryEntityRepositoryRead; + +[ScopedService] +internal class NexusModsModToModuleInfoHistoryEntityRepository : Repository, INexusModsModToModuleInfoHistoryEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsMod).ThenInclude(x => x.Name) + .Include(x => x.Module); + + public NexusModsModToModuleInfoHistoryEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsModToNameEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsModToNameEntityRepository.cs new file mode 100644 index 00000000..326a2bfa --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsModToNameEntityRepository.cs @@ -0,0 +1,22 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface INexusModsModToNameEntityRepositoryRead : IRepositoryRead; +public interface INexusModsModToNameEntityRepositoryWrite : IRepositoryWrite, INexusModsModToNameEntityRepositoryRead; + +[ScopedService] + +internal class NexusModsModToNameEntityRepository : Repository, INexusModsModToNameEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsMod).ThenInclude(x => x.Name); + + public NexusModsModToNameEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserRepository.cs new file mode 100644 index 00000000..ada0cd56 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserRepository.cs @@ -0,0 +1,161 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Extensions; +using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Models.API; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public sealed record UserAvailableModModel +{ + public required NexusModsModId NexusModsModId { get; init; } + public required string Name { get; init; } +} + +public sealed record UserLinkedModModel +{ + public required NexusModsModId NexusModsModId { get; init; } + public required string Name { get; init; } + public required NexusModsUserId[] OwnerNexusModsUserIds { get; init; } + public required NexusModsUserId[] AllowedNexusModsUserIds { get; init; } + public required NexusModsUserId[] ManuallyLinkedNexusModsUserIds { get; init; } + public required ModuleId[] ManuallyLinkedModuleIds { get; init; } + public required ModuleId[] KnownModuleIds { get; init; } +} + +public sealed record ModuleIdToVersionModel +{ + public required ModuleId ModuleId { get; init; } + public required ModuleVersion Version { get; init; } +} +public sealed record UserCrashReportModel +{ + public required CrashReportId Id { get; init; } + public required CrashReportVersion Version { get; init; } + public required GameVersion GameVersion { get; init; } + public required ExceptionTypeId ExceptionType { get; init; } + public required string Exception { get; init; } + public required DateTimeOffset CreatedAt { get; init; } + //public required ModuleId[] ModuleIds { get; init; } + //public required ModuleIdToVersionModel[] ModuleIdToVersion { get; init; } + //public required ModuleId? TopInvolvedModuleId { get; init; } + public required ModuleId[] InvolvedModuleIds { get; init; } + //public required NexusModsModId[] NexusModsModIds { get; init; } + public required CrashReportUrl Url { get; init; } + + public required CrashReportStatus Status { get; init; } + public required string? Comment { get; init; } +} + +public interface INexusModsUserRepositoryRead : IRepositoryRead +{ + Task GetUserWithIntegrationsAsync(NexusModsUserId userId, CancellationToken ct); + Task GetUserAsync(NexusModsUserId userId, CancellationToken ct); + + Task> GetNexusModsModsPaginatedAsync(NexusModsUserId userId, PaginatedQuery query, CancellationToken ct); + + Task> GetAvailableModsPaginatedAsync(NexusModsUserId userId, PaginatedQuery query, CancellationToken ct); + + Task GetLinkedModCountAsync(NexusModsUserId userId, CancellationToken ct); +} +public interface INexusModsUserRepositoryWrite : IRepositoryWrite, INexusModsUserRepositoryRead; + +[ScopedService] +internal class NexusModsUserRepository : Repository, INexusModsUserRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.Name) + .Include(x => x.ToRoles) + .Include(x => x.ToModules) + .Include(x => x.ToNexusModsMods) + .Include(x => x.ToCrashReports) + .Include(x => x.ToArticles) + .Include(x => x.ToGitHub) + .Include(x => x.ToDiscord) + .Include(x => x.ToSteam) + .Include(x => x.ToGOG); + + public NexusModsUserRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } + + public async Task GetUserAsync(NexusModsUserId userId, CancellationToken ct) => await _dbContext.NexusModsUsers + .Include(x => x.ToModules).ThenInclude(x => x.Module) + .Include(x => x.ToNexusModsMods).ThenInclude(x => x.NexusModsMod) + .AsSplitQuery() + .FirstAsync(x => x.NexusModsUserId == userId, ct); + + public async Task GetUserWithIntegrationsAsync(NexusModsUserId userId, CancellationToken ct) => await _dbContext.NexusModsUsers + .Include(x => x.ToRoles) + .Include(x => x.ToGitHub!).ThenInclude(x => x.ToTokens) + .Include(x => x.ToDiscord!).ThenInclude(x => x.ToTokens) + .Include(x => x.ToGOG!).ThenInclude(x => x.ToTokens) + .Include(x => x.ToGOG!).ThenInclude(x => x.ToOwnedTenants) + .Include(x => x.ToSteam!).ThenInclude(x => x.ToTokens) + .Include(x => x.ToSteam!).ThenInclude(x => x.ToOwnedTenants) + .AsSplitQuery() + .FirstOrDefaultAsync(x => x.NexusModsUserId == userId, ct); + + public async Task> GetNexusModsModsPaginatedAsync(NexusModsUserId userId, PaginatedQuery query, CancellationToken ct) + { + var availableModsByNexusModsModLinkage = _dbContext.NexusModsUsers + .Include(x => x.ToNexusModsMods).ThenInclude(x => x.NexusModsMod).ThenInclude(x => x.Name) + .Include(x => x.ToNexusModsMods).ThenInclude(x => x.NexusModsMod).ThenInclude(x => x.ToNexusModsUsers).ThenInclude(x => x.NexusModsUser) + .Include(x => x.ToNexusModsMods).ThenInclude(x => x.NexusModsMod).ThenInclude(x => x.ModuleIds).ThenInclude(x => x.Module) + .Where(x => x.NexusModsUserId == userId) + .SelectMany(x => x.ToNexusModsMods) + .Select(x => x.NexusModsMod) + .AsSplitQuery() + .Select(x => new UserLinkedModModel + { + NexusModsModId = x.NexusModsModId, + Name = x.Name!.Name, + OwnerNexusModsUserIds = x.ToNexusModsUsers.Where(y => y.NexusModsUser.NexusModsUserId != userId && y.LinkType == NexusModsUserToNexusModsModLinkType.ByAPIConfirmation).Select(y => y.NexusModsUser.NexusModsUserId).ToArray(), + AllowedNexusModsUserIds = x.ToNexusModsUsers.Where(y => y.NexusModsUser.NexusModsUserId != userId && y.LinkType == NexusModsUserToNexusModsModLinkType.ByOwner || y.LinkType == NexusModsUserToNexusModsModLinkType.ByStaff).Select(y => y.NexusModsUser.NexusModsUserId).ToArray(), + ManuallyLinkedNexusModsUserIds = x.ToNexusModsUsers.Where(y => y.NexusModsUser.NexusModsUserId != userId && y.LinkType == NexusModsUserToNexusModsModLinkType.ByOwner).Select(y => y.NexusModsUser.NexusModsUserId).ToArray(), + ManuallyLinkedModuleIds = x.ModuleIds.Where(y => y.LinkType == NexusModsModToModuleLinkType.ByStaff).Select(y => y.Module.ModuleId).ToArray(), + KnownModuleIds = x.ModuleIds.Where(y => y.LinkType == NexusModsModToModuleLinkType.ByUnverifiedFileExposure).Select(y => y.Module.ModuleId).ToArray(), + }); + + return await availableModsByNexusModsModLinkage + .PaginatedAsync(query, 20, new() { Property = nameof(UserLinkedModModel.NexusModsModId), Type = SortingType.Ascending }, ct); + } + + public async Task> GetAvailableModsPaginatedAsync(NexusModsUserId userId, PaginatedQuery query, CancellationToken ct) + { + var userToModIds = _dbContext.NexusModsUserToNexusModsMods + .Include(x => x.NexusModsMod).ThenInclude(x => x.Name) + .Where(x => x.NexusModsUser.NexusModsUserId == userId) + .Select(x => x.NexusModsMod); + + var userToModuleIdsToModIds = _dbContext.NexusModsUserToModules + .Include(x => x.Module).ThenInclude(x => x.ToNexusModsMods).ThenInclude(x => x.NexusModsMod).ThenInclude(x => x.Name) + .AsSplitQuery() + .Where(x => x.NexusModsUser.NexusModsUserId == userId) + .Select(x => x.Module) + .SelectMany(x => x.ToNexusModsMods) + .Select(x => x.NexusModsMod); + + return await userToModIds.Union(userToModuleIdsToModIds) + .Select(x => new UserAvailableModModel + { + NexusModsModId = x.NexusModsModId, + Name = x.Name!.Name, + }) + .PaginatedAsync(query, 20, new() { Property = nameof(NexusModsModEntity.NexusModsModId), Type = SortingType.Ascending }, ct); + } + + + public async Task GetLinkedModCountAsync(NexusModsUserId userId, CancellationToken ct) => await _dbContext.NexusModsUsers + .Include(x => x.ToNexusModsMods) + .AsSplitQuery() + .Where(x => x.NexusModsUserId == userId) + .SelectMany(x => x.ToNexusModsMods) + .CountAsync(ct); +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToCrashReportEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToCrashReportEntityRepository.cs new file mode 100644 index 00000000..890d0b05 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToCrashReportEntityRepository.cs @@ -0,0 +1,22 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface INexusModsUserToCrashReportEntityRepositoryRead : IRepositoryRead; +public interface INexusModsUserToCrashReportEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToCrashReportEntityRepositoryRead; + +[ScopedService] +internal class NexusModsUserToCrashReportEntityRepository : Repository, INexusModsUserToCrashReportEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsUser) + .Include(x => x.ToCrashReport); + + public NexusModsUserToCrashReportEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToIntegrationDiscordEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToIntegrationDiscordEntityRepository.cs new file mode 100644 index 00000000..649fce0b --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToIntegrationDiscordEntityRepository.cs @@ -0,0 +1,22 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface INexusModsUserToIntegrationDiscordEntityRepositoryRead : IRepositoryRead; +public interface INexusModsUserToIntegrationDiscordEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToIntegrationDiscordEntityRepositoryRead; + +[ScopedService] +internal class NexusModsUserToIntegrationDiscordEntityRepository : Repository, INexusModsUserToIntegrationDiscordEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsUser) + .Include(x => x.ToTokens); + + public NexusModsUserToIntegrationDiscordEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToIntegrationGOGEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToIntegrationGOGEntityRepository.cs new file mode 100644 index 00000000..9fee5325 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToIntegrationGOGEntityRepository.cs @@ -0,0 +1,23 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface INexusModsUserToIntegrationGOGEntityRepositoryRead : IRepositoryRead; +public interface INexusModsUserToIntegrationGOGEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToIntegrationGOGEntityRepositoryRead; + +[ScopedService] +internal class NexusModsUserToIntegrationGOGEntityRepository : Repository, INexusModsUserToIntegrationGOGEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsUser) + .Include(x => x.ToTokens) + .Include(x => x.ToOwnedTenants); + + public NexusModsUserToIntegrationGOGEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToIntegrationGitHubEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToIntegrationGitHubEntityRepository.cs new file mode 100644 index 00000000..2e790d1f --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToIntegrationGitHubEntityRepository.cs @@ -0,0 +1,22 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface INexusModsUserToIntegrationGitHubEntityRepositoryRead : IRepositoryRead; +public interface INexusModsUserToIntegrationGitHubEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToIntegrationGitHubEntityRepositoryRead; + +[ScopedService] +internal class NexusModsUserToIntegrationGitHubEntityRepository : Repository, INexusModsUserToIntegrationGitHubEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsUser) + .Include(x => x.ToTokens); + + public NexusModsUserToIntegrationGitHubEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToIntegrationSteamEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToIntegrationSteamEntityRepository.cs new file mode 100644 index 00000000..d74d4b3c --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToIntegrationSteamEntityRepository.cs @@ -0,0 +1,23 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface INexusModsUserToIntegrationSteamEntityRepositoryRead : IRepositoryRead; +public interface INexusModsUserToIntegrationSteamEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToIntegrationSteamEntityRepositoryRead; + +[ScopedService] +internal class NexusModsUserToIntegrationSteamEntityRepository : Repository, INexusModsUserToIntegrationSteamEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsUser) + .Include(x => x.ToTokens) + .Include(x => x.ToOwnedTenants); + + public NexusModsUserToIntegrationSteamEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToModuleEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToModuleEntityRepository.cs new file mode 100644 index 00000000..49a4801a --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToModuleEntityRepository.cs @@ -0,0 +1,53 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Extensions; +using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Models.API; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public sealed record UserManuallyLinkedModuleModel +{ + public required NexusModsUserId NexusModsUserId { get; init; } + public required NexusModsUserName NexusModsUsername { get; init; } + public required ModuleId[] ModuleIds { get; init; } +} + +public interface INexusModsUserToModuleEntityRepositoryRead : IRepositoryRead +{ + Task> GetManuallyLinkedModuleIdsPaginatedAsync(PaginatedQuery query, NexusModsUserToModuleLinkType linkType, CancellationToken ct); +} +public interface INexusModsUserToModuleEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToModuleEntityRepositoryRead; + +[ScopedService] +internal class NexusModsUserToModuleEntityRepository : Repository, INexusModsUserToModuleEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsUser) + .Include(x => x.Module); + + public NexusModsUserToModuleEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } + + public async Task> GetManuallyLinkedModuleIdsPaginatedAsync(PaginatedQuery query, NexusModsUserToModuleLinkType linkType, CancellationToken ct) + { + var unknown = NexusModsUserName.From("UNKNOWN"); + return await _dbContext.NexusModsUserToModules + .Include(x => x.NexusModsUser).ThenInclude(x => x.Name) + .Where(x => x.LinkType == linkType) + .GroupBy(x => new { x.NexusModsUser.NexusModsUserId, x.NexusModsUser.Name!.Name }) + .Select(x => new UserManuallyLinkedModuleModel + { + NexusModsUserId = x.Key.NexusModsUserId, + NexusModsUsername = x.Select(y => y.NexusModsUser.Name == null ? unknown : y.NexusModsUser.Name.Name).First(), + ModuleIds = x.Select(y => y.Module.ModuleId).ToArray(), + }) + .PaginatedAsync(query, 20, new() { Property = nameof(NexusModsUserEntity.NexusModsUserId), Type = SortingType.Ascending }, ct); + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToNameEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToNameEntityRepository.cs new file mode 100644 index 00000000..7924a0e6 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToNameEntityRepository.cs @@ -0,0 +1,21 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface INexusModsUserToNameEntityRepositoryRead : IRepositoryRead; +public interface INexusModsUserToNameEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToNameEntityRepositoryRead; + +[ScopedService] +internal class NexusModsUserToNameEntityRepository : Repository, INexusModsUserToNameEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsUser); + + public NexusModsUserToNameEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToNexusModsModEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToNexusModsModEntityRepository.cs new file mode 100644 index 00000000..2448ba0b --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsUserToNexusModsModEntityRepository.cs @@ -0,0 +1,56 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Extensions; +using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Models.API; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public sealed record UserManuallyLinkedModUserModel +{ + public required NexusModsUserId NexusModsUserId { get; init; } + public required NexusModsUserName NexusModsUsername { get; init; } +} +public sealed record UserManuallyLinkedModModel +{ + public required NexusModsModId NexusModsModId { get; init; } + public required UserManuallyLinkedModUserModel[] NexusModsUsers { get; init; } +} + +public interface INexusModsUserToNexusModsModEntityRepositoryRead : IRepositoryRead +{ + Task> GetManuallyLinkedPaginatedAsync(NexusModsUserId userId, PaginatedQuery query, CancellationToken ct); +} +public interface INexusModsUserToNexusModsModEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToNexusModsModEntityRepositoryRead; + +[ScopedService] +internal class NexusModsUserToNexusModsModEntityRepository : Repository, INexusModsUserToNexusModsModEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.NexusModsUser) + .Include(x => x.NexusModsMod); + + public NexusModsUserToNexusModsModEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } + + public async Task> GetManuallyLinkedPaginatedAsync(NexusModsUserId userId, PaginatedQuery query, CancellationToken ct) => await _dbContext.NexusModsUserToNexusModsMods + .Include(x => x.NexusModsUser).ThenInclude(x => x.Name) + .Where(x => x.NexusModsUser.NexusModsUserId == userId && x.LinkType == NexusModsUserToNexusModsModLinkType.ByOwner) + .GroupBy(x => new { x.NexusModsMod.NexusModsModId }) + .Select(x => new UserManuallyLinkedModModel + { + NexusModsModId = x.Key.NexusModsModId, + NexusModsUsers = x.Select(y => new UserManuallyLinkedModUserModel + { + NexusModsUserId = y.NexusModsUser.NexusModsUserId, + NexusModsUsername = y.NexusModsUser.Name!.Name, + }).ToArray(), + }) + .PaginatedAsync(query, 20, new() { Property = nameof(NexusModsModEntity.NexusModsModId), Type = SortingType.Ascending }, ct); +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/QuartzExecutionLogEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/QuartzExecutionLogEntityRepository.cs new file mode 100644 index 00000000..3fdcdebb --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/QuartzExecutionLogEntityRepository.cs @@ -0,0 +1,32 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface IQuartzExecutionLogEntityRepositoryRead : IRepositoryRead; + +public interface IQuartzExecutionLogEntityRepositoryWrite : IRepositoryWrite, IQuartzExecutionLogEntityRepositoryRead +{ + Task MarkIncompleteAsync(CancellationToken ct); +} + +[ScopedService] +internal class QuartzExecutionLogEntityRepository : Repository, IQuartzExecutionLogEntityRepositoryWrite +{ + public QuartzExecutionLogEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } + + public async Task MarkIncompleteAsync(CancellationToken ct) => await _dbContext.QuartzExecutionLogs + .Where(x => !x.IsSuccess.HasValue) + .ExecuteUpdateAsync(calls => calls + .SetProperty(x => x.IsSuccess, false) + .SetProperty(x => x.ErrorMessage, "Incomplete execution.") + .SetProperty(x => x.JobRunTime, TimeSpan.Zero), ct); +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/StatisticsCrashScoreInvolvedEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/StatisticsCrashScoreInvolvedEntityRepository.cs new file mode 100644 index 00000000..e916402f --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/StatisticsCrashScoreInvolvedEntityRepository.cs @@ -0,0 +1,111 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Extensions; +using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public sealed record VersionScoreModel +{ + public required ModuleVersion Version { get; init; } + public required double Score { get; init; } + public required double Value { get; init; } + public required int CountStable { get; init; } + public required int CountUnstable { get; init; } + public double Count => CountStable + CountUnstable; +} +public sealed record VersionStorageModel +{ + public required ModuleVersion Version { get; init; } + public required VersionScoreModel[] Scores { get; init; } + public double MeanScore => Scores.Length == 0 ? 0 : 1 - (Scores.Sum(x => x.Value) / (double) Scores.Sum(x => x.Count)); +}; +public sealed record ModuleStorageModel +{ + public required ModuleId ModuleId { get; init; } + public required VersionStorageModel[] Versions { get; init; } +}; +public sealed record StatisticsInvolvedModuleScoresForGameVersionModel +{ + public required GameVersion GameVersion { get; init; } + public required ModuleStorageModel[] Modules { get; init; } +} + +public sealed record RawScoreForModuleVersionModel +{ + public required ModuleVersion ModuleVersion { get; init; } + public required double RawScore { get; init; } + public required int TotalCount { get; init; } +} + +public sealed record StatisticsRawScoresForModuleModel +{ + public required ModuleId ModuleId { get; init; } + public required RawScoreForModuleVersionModel[] RawScores { get; init; } +} + +public interface IStatisticsCrashScoreInvolvedEntityRepositoryRead : IRepositoryRead +{ + Task> GetAllInvolvedModuleScoresForGameVersionAsync(GameVersion[]? gameVersions, ModuleId[]? moduleIds, ModuleVersion[]? moduleVersions, CancellationToken ct); + Task> GetAllRawScoresForAllModulesAsync(GameVersion gameVersion, ModuleId[] moduleIds, CancellationToken ct); + +} +public interface IStatisticsCrashScoreInvolvedEntityRepositoryWrite : IRepositoryWrite, IStatisticsCrashScoreInvolvedEntityRepositoryRead; + +[ScopedService] +internal class StatisticsCrashScoreInvolvedEntityRepository : Repository, IStatisticsCrashScoreInvolvedEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.Module); + + public StatisticsCrashScoreInvolvedEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } + + public async Task> GetAllInvolvedModuleScoresForGameVersionAsync(GameVersion[]? gameVersions, ModuleId[]? moduleIds, ModuleVersion[]? moduleVersions, CancellationToken ct) => await _dbContext.StatisticsCrashScoreInvolveds + .Include(x => x.Module) + .WhereIf(gameVersions != null && gameVersions.Length != 0, x => gameVersions!.Contains(x.GameVersion)) + .WhereIf(moduleIds != null && moduleIds.Length != 0, x => moduleIds!.Contains(x.Module.ModuleId)) + .WhereIf(moduleVersions != null && moduleVersions.Length != 0, x => moduleVersions!.Contains(x.ModuleVersion)) + .GroupBy(x => new { x.GameVersion }) + .Select(x => new StatisticsInvolvedModuleScoresForGameVersionModel + { + GameVersion = x.Key.GameVersion, + Modules = x.GroupBy(y => new { y.Module.ModuleId }).Select(y => new ModuleStorageModel + { + ModuleId = y.Key.ModuleId, + Versions = y.GroupBy(z => new { z.ModuleVersion }).Select(z => new VersionStorageModel + { + Version = z.Key.ModuleVersion, + Scores = z.Select(q => new VersionScoreModel + { + Version = z.Key.ModuleVersion, + Score = 1 - q.Score, + Value = q.RawValue, + CountStable = q.NotInvolvedCount, + CountUnstable = q.InvolvedCount, + }).ToArray(), + }).ToArray(), + }).ToArray(), + }).ToListAsync(ct); + + public async Task> GetAllRawScoresForAllModulesAsync(GameVersion gameVersion, ModuleId[] moduleIds, CancellationToken ct) => await _dbContext.StatisticsCrashScoreInvolveds + .Where(x => x.GameVersion == gameVersion && moduleIds.Contains(x.Module.ModuleId)) + .GroupBy(x => x.Module.ModuleId) + .Select(x => new StatisticsRawScoresForModuleModel + { + ModuleId = x.Key, + RawScores = x.OrderBy(y => y.Score).Select(y => new RawScoreForModuleVersionModel + { + ModuleVersion = y.ModuleVersion, + RawScore = y.Score, + TotalCount = y.TotalCount, + }).Take(10).ToArray(), + }).ToListAsync(ct); +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/StatisticsTopExceptionsTypeEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/StatisticsTopExceptionsTypeEntityRepository.cs new file mode 100644 index 00000000..407b063e --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Repositories/StatisticsTopExceptionsTypeEntityRepository.cs @@ -0,0 +1,21 @@ +using BUTR.Site.NexusMods.DependencyInjection; +using BUTR.Site.NexusMods.Server.Contexts; +using BUTR.Site.NexusMods.Server.Models.Database; + +using Microsoft.EntityFrameworkCore; + +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Repositories; + +public interface IStatisticsTopExceptionsTypeEntityRepositoryRead : IRepositoryRead; +public interface IStatisticsTopExceptionsTypeEntityRepositoryWrite : IRepositoryWrite, IStatisticsTopExceptionsTypeEntityRepositoryRead; + +[ScopedService] +internal class StatisticsTopExceptionsTypeEntityRepository : Repository, IStatisticsTopExceptionsTypeEntityRepositoryWrite +{ + protected override IQueryable InternalQuery => base.InternalQuery + .Include(x => x.ExceptionType); + + public StatisticsTopExceptionsTypeEntityRepository(IAppDbContextProvider appDbContextProvider) : base(appDbContextProvider.Get()) { } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/IDiscordStorage.cs b/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/IDiscordStorage.cs index 030340fd..f78a6c6d 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/IDiscordStorage.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/IDiscordStorage.cs @@ -1,12 +1,10 @@ using BUTR.Site.NexusMods.DependencyInjection; -using BUTR.Site.NexusMods.Server.Contexts; -using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; - -using Microsoft.EntityFrameworkCore; +using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; using System; -using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace BUTR.Site.NexusMods.Server.Services; @@ -23,40 +21,57 @@ public interface IDiscordStorage [ScopedService] public sealed class DatabaseDiscordStorage : IDiscordStorage { - private readonly IAppDbContextRead _dbContextRead; - private readonly IAppDbContextWrite _dbContextWrite; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; - public DatabaseDiscordStorage(IAppDbContextRead dbContextRead, IAppDbContextWrite dbContextWrite) + public DatabaseDiscordStorage(IUnitOfWorkFactory unitOfWorkFactory) { - _dbContextRead = dbContextRead; - _dbContextWrite = dbContextWrite; + _unitOfWorkFactory = unitOfWorkFactory; } public async Task GetAsync(string discordUserId) { - var entity = await _dbContextRead.IntegrationDiscordTokens.FirstOrDefaultAsync(x => x.DiscordUserId.Equals(discordUserId)); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(TenantId.None); + + var entity = await unitOfRead.IntegrationDiscordTokens.FirstOrDefaultAsync(x => x.DiscordUserId.Equals(discordUserId), null, CancellationToken.None); if (entity is null) return null; return new(entity.AccessToken, entity.RefreshToken, entity.AccessTokenExpiresAt); } public async Task UpsertAsync(NexusModsUserId nexusModsUserId, string discordUserId, DiscordOAuthTokens tokens) { - var entityFactory = _dbContextWrite.GetEntityFactory(); - await using var _ = await _dbContextWrite.CreateSaveScopeAsync(); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(TenantId.None); - var nexusModsUserToIntegrationDiscord = entityFactory.GetOrCreateNexusModsUserDiscord(nexusModsUserId, discordUserId); - var tokensDiscord = entityFactory.GetOrCreateIntegrationDiscordTokens(nexusModsUserId, discordUserId, tokens.AccessToken, tokens.RefreshToken, tokens.ExpiresAt); + var nexusModsUserToIntegrationDiscord = new NexusModsUserToIntegrationDiscordEntity + { + NexusModsUserId = nexusModsUserId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(nexusModsUserId), + DiscordUserId = discordUserId, + }; + var tokensDiscord = new IntegrationDiscordTokensEntity + { + DiscordUserId = discordUserId, + NexusModsUserId = nexusModsUserId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(nexusModsUserId), + AccessToken = tokens.AccessToken, + RefreshToken = tokens.RefreshToken, + AccessTokenExpiresAt = tokens.ExpiresAt, + }; - await _dbContextWrite.NexusModsUserToDiscord.UpsertOnSaveAsync(nexusModsUserToIntegrationDiscord); - await _dbContextWrite.IntegrationDiscordTokens.UpsertOnSaveAsync(tokensDiscord); + unitOfWrite.NexusModsUserToDiscord.Upsert(nexusModsUserToIntegrationDiscord); + unitOfWrite.IntegrationDiscordTokens.Upsert(tokensDiscord); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return true; } public async Task RemoveAsync(NexusModsUserId nexusModsUserId, string discordUserId) { - await _dbContextWrite.NexusModsUserToDiscord.Where(x => x.NexusModsUser.NexusModsUserId == nexusModsUserId && x.DiscordUserId == discordUserId).ExecuteDeleteAsync(); - await _dbContextWrite.IntegrationDiscordTokens.Where(x => x.DiscordUserId == discordUserId).ExecuteDeleteAsync(); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(TenantId.None); + + unitOfWrite.NexusModsUserToDiscord.Remove(x => x.NexusModsUser.NexusModsUserId == nexusModsUserId && x.DiscordUserId == discordUserId); + unitOfWrite.IntegrationDiscordTokens.Remove(x => x.DiscordUserId == discordUserId); + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return true; } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/IGOGStorage.cs b/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/IGOGStorage.cs index 2bd2f6a1..e20a6c35 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/IGOGStorage.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/IGOGStorage.cs @@ -1,10 +1,7 @@ using BUTR.Site.NexusMods.DependencyInjection; -using BUTR.Site.NexusMods.Server.Contexts; -using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.Database; - -using Microsoft.EntityFrameworkCore; +using BUTR.Site.NexusMods.Server.Repositories; using System; using System.Collections.Generic; @@ -30,32 +27,34 @@ public sealed class DatabaseGOGStorage : IGOGStorage { private Dictionary> TenantToGameIds { get; } = new() { - { TenantId.Bannerlord, [1802539526, 1564781494]}, - { TenantId.Rimworld, [1094900565]}, - { TenantId.StardewValley, [1453375253]}, + { TenantId.Bannerlord, [1802539526, 1564781494] }, + { TenantId.Rimworld, [1094900565] }, + { TenantId.StardewValley, [1453375253] }, }; - private readonly IAppDbContextRead _dbContextRead; - private readonly IAppDbContextWrite _dbContextWrite; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; private readonly IGOGEmbedClient _gogEmbedClient; - public DatabaseGOGStorage(IAppDbContextRead dbContextRead, IAppDbContextWrite dbContextWrite, IGOGEmbedClient gogEmbedClient) + public DatabaseGOGStorage(IUnitOfWorkFactory unitOfWorkFactory, IGOGEmbedClient gogEmbedClient) { - _dbContextRead = dbContextRead; - _dbContextWrite = dbContextWrite; + _unitOfWorkFactory = unitOfWorkFactory; _gogEmbedClient = gogEmbedClient; } public async Task CheckOwnedGamesAsync(NexusModsUserId nexusModsUserId, string gogUserId, GOGOAuthTokens tokens) { - var entityFactory = _dbContextWrite.GetEntityFactory(); - await using var _ = await _dbContextWrite.CreateSaveScopeAsync(); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(TenantId.None); var games = await _gogEmbedClient.GetGamesAsync(tokens.AccessToken, CancellationToken.None); if (games is null) return false; - var nexusModsUserToIntegrationGOG = entityFactory.GetOrCreateNexusModsUserGOG(nexusModsUserId, gogUserId); + var nexusModsUserToIntegrationGOG = new NexusModsUserToIntegrationGOGEntity + { + NexusModsUserId = nexusModsUserId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(nexusModsUserId), + GOGUserId = gogUserId, + }; var ownedTenants = TenantToGameIds.Where(x => x.Value.Intersect(games.Owned.Where(y => y.HasValue).Select(y => y!.Value)).Any()); var list = ownedTenants.Select(x => x.Key).Select(x => new IntegrationGOGToOwnedTenantEntity @@ -64,37 +63,58 @@ public async Task CheckOwnedGamesAsync(NexusModsUserId nexusModsUserId, st OwnedTenant = x, }).ToArray(); - await _dbContextWrite.NexusModsUserToGOG.UpsertOnSaveAsync(nexusModsUserToIntegrationGOG); - await _dbContextWrite.IntegrationGOGToOwnedTenants.UpsertOnSaveAsync(list); - await _dbContextWrite.IntegrationGOGToOwnedTenants.Where(x => x.GOGUserId == gogUserId).ExecuteDeleteAsync(CancellationToken.None); + unitOfWrite.NexusModsUserToGOG.Upsert(nexusModsUserToIntegrationGOG); + unitOfWrite.IntegrationGOGToOwnedTenants.UpsertRange(list); + unitOfWrite.IntegrationGOGToOwnedTenants.Remove(x => x.GOGUserId == gogUserId); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return true; } public async Task GetAsync(string gogUserId) { - var entity = await _dbContextRead.IntegrationGOGTokens.FirstOrDefaultAsync(x => x.GOGUserId.Equals(gogUserId)); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(TenantId.None); + + var entity = await unitOfRead.IntegrationGOGTokens.FirstOrDefaultAsync(x => x.GOGUserId.Equals(gogUserId), null, CancellationToken.None); if (entity is null) return null; return new(gogUserId, entity.AccessToken, entity.RefreshToken, entity.AccessTokenExpiresAt); } public async Task UpsertAsync(NexusModsUserId nexusModsUserId, string gogUserId, GOGOAuthTokens tokens) { - var entityFactory = _dbContextWrite.GetEntityFactory(); - await using var _ = await _dbContextWrite.CreateSaveScopeAsync(); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(TenantId.None); - var nexusModsUserToIntegrationGOG = entityFactory.GetOrCreateNexusModsUserGOG(nexusModsUserId, gogUserId); - var tokensGOG = entityFactory.GetOrCreateIntegrationGOGTokens(nexusModsUserId, gogUserId, tokens.AccessToken, tokens.RefreshToken, tokens.ExpiresAt); + var nexusModsUserToIntegrationGOG = new NexusModsUserToIntegrationGOGEntity + { + NexusModsUserId = nexusModsUserId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(nexusModsUserId), + GOGUserId = gogUserId, + }; + var tokensGOG = new IntegrationGOGTokensEntity + { + GOGUserId = gogUserId, + NexusModsUserId = nexusModsUserId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(nexusModsUserId), + AccessToken = tokens.AccessToken, + RefreshToken = tokens.RefreshToken, + AccessTokenExpiresAt = tokens.ExpiresAt, + }; - await _dbContextWrite.NexusModsUserToGOG.UpsertOnSaveAsync(nexusModsUserToIntegrationGOG); - await _dbContextWrite.IntegrationGOGTokens.UpsertOnSaveAsync(tokensGOG); + unitOfWrite.NexusModsUserToGOG.Upsert(nexusModsUserToIntegrationGOG); + unitOfWrite.IntegrationGOGTokens.Upsert(tokensGOG); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return true; } public async Task RemoveAsync(NexusModsUserId nexusModsUserId, string gogUserId) { - await _dbContextWrite.NexusModsUserToGOG.Where(x => x.NexusModsUser.NexusModsUserId == nexusModsUserId && x.GOGUserId == gogUserId).ExecuteDeleteAsync(); - await _dbContextWrite.IntegrationGOGTokens.Where(x => x.GOGUserId == gogUserId).ExecuteDeleteAsync(); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(TenantId.None); + + unitOfWrite.NexusModsUserToGOG.Remove(x => x.NexusModsUser.NexusModsUserId == nexusModsUserId && x.GOGUserId == gogUserId); + unitOfWrite.IntegrationGOGTokens.Remove(x => x.GOGUserId == gogUserId); + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return true; } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/IGitHubStorage.cs b/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/IGitHubStorage.cs index c306c579..10573155 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/IGitHubStorage.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/IGitHubStorage.cs @@ -1,11 +1,9 @@ using BUTR.Site.NexusMods.DependencyInjection; -using BUTR.Site.NexusMods.Server.Contexts; -using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Models.Database; +using BUTR.Site.NexusMods.Server.Repositories; -using Microsoft.EntityFrameworkCore; - -using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace BUTR.Site.NexusMods.Server.Services; @@ -22,39 +20,55 @@ public interface IGitHubStorage [ScopedService] public sealed class DatabaseGitHubStorage : IGitHubStorage { - private readonly IAppDbContextRead _dbContextRead; - private readonly IAppDbContextWrite _dbContextWrite; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; - public DatabaseGitHubStorage(IAppDbContextRead dbContextRead, IAppDbContextWrite dbContextWrite) + public DatabaseGitHubStorage(IUnitOfWorkFactory unitOfWorkFactory) { - _dbContextRead = dbContextRead; - _dbContextWrite = dbContextWrite; + _unitOfWorkFactory = unitOfWorkFactory; } public async Task GetAsync(string gitHubUserId) { - var entity = await _dbContextRead.IntegrationGitHubTokens.FirstOrDefaultAsync(x => x.GitHubUserId.Equals(gitHubUserId)); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(TenantId.None); + + var entity = await unitOfRead.IntegrationGitHubTokens.FirstOrDefaultAsync(x => x.GitHubUserId.Equals(gitHubUserId), null, CancellationToken.None); if (entity is null) return null; return new(entity.AccessToken); } public async Task UpsertAsync(NexusModsUserId nexusModsUserId, string gitHubUserId, GitHubOAuthTokens tokens) { - var entityFactory = _dbContextWrite.GetEntityFactory(); - await using var _ = await _dbContextWrite.CreateSaveScopeAsync(); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(TenantId.None); + + var nexusModsUserToIntegrationGitHub = new NexusModsUserToIntegrationGitHubEntity + { + NexusModsUserId = nexusModsUserId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(nexusModsUserId), + GitHubUserId = gitHubUserId, + }; + var tokensGitHub = new IntegrationGitHubTokensEntity + { + GitHubUserId = gitHubUserId, + NexusModsUserId = nexusModsUserId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(nexusModsUserId), + AccessToken = tokens.AccessToken, + }; - var nexusModsUserToIntegrationGitHub = entityFactory.GetOrCreateNexusModsUserGitHub(nexusModsUserId, gitHubUserId); - var tokensGitHub = entityFactory.GetOrCreateIntegrationGitHubTokens(nexusModsUserId, gitHubUserId, tokens.AccessToken); + unitOfWrite.NexusModsUserToGitHub.Upsert(nexusModsUserToIntegrationGitHub); + unitOfWrite.IntegrationGitHubTokens.Upsert(tokensGitHub); - await _dbContextWrite.NexusModsUserToGitHub.UpsertOnSaveAsync(nexusModsUserToIntegrationGitHub); - await _dbContextWrite.IntegrationGitHubTokens.UpsertOnSaveAsync(tokensGitHub); + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return true; } public async Task RemoveAsync(NexusModsUserId nexusModsUserId, string gitHubUserId) { - await _dbContextWrite.NexusModsUserToGitHub.Where(x => x.NexusModsUser.NexusModsUserId == nexusModsUserId && x.GitHubUserId == gitHubUserId).ExecuteDeleteAsync(); - await _dbContextWrite.IntegrationGitHubTokens.Where(x => x.GitHubUserId == gitHubUserId).ExecuteDeleteAsync(); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(TenantId.None); + + unitOfWrite.NexusModsUserToGitHub.Remove(x => x.NexusModsUser.NexusModsUserId == nexusModsUserId && x.GitHubUserId == gitHubUserId); + unitOfWrite.IntegrationGitHubTokens.Remove(x => x.GitHubUserId == gitHubUserId); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return true; } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/ISteamStorage.cs b/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/ISteamStorage.cs index 2392d92e..6b6b0d02 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/ISteamStorage.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/ExternalStorage/ISteamStorage.cs @@ -1,14 +1,10 @@ using BUTR.Site.NexusMods.DependencyInjection; -using BUTR.Site.NexusMods.Server.Contexts; -using BUTR.Site.NexusMods.Server.Extensions; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.Database; - -using Microsoft.EntityFrameworkCore; +using BUTR.Site.NexusMods.Server.Repositories; using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -28,28 +24,31 @@ public sealed class DatabaseSteamStorage : ISteamStorage { private Dictionary> TenantToGameIds { get; } = new() { - { TenantId.Bannerlord, [261550]}, - { TenantId.Rimworld, [294100]}, - { TenantId.StardewValley, [413150]}, + { TenantId.Bannerlord, [261550] }, + { TenantId.Rimworld, [294100] }, + { TenantId.StardewValley, [413150] }, + { TenantId.Valheim, [892970] }, }; - private readonly IAppDbContextRead _dbContextRead; - private readonly IAppDbContextWrite _dbContextWrite; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; private readonly ISteamAPIClient _steamAPIClient; - public DatabaseSteamStorage(IAppDbContextRead dbContextRead, IAppDbContextWrite dbContextWrite, ISteamAPIClient steamAPIClient) + public DatabaseSteamStorage(IUnitOfWorkFactory unitOfWorkFactory, ISteamAPIClient steamAPIClient) { - _dbContextRead = dbContextRead; - _dbContextWrite = dbContextWrite; + _unitOfWorkFactory = unitOfWorkFactory; _steamAPIClient = steamAPIClient; } public async Task CheckOwnedGamesAsync(NexusModsUserId nexusModsUserId, string steamUserId) { - var entityFactory = _dbContextWrite.GetEntityFactory(); - await using var _ = await _dbContextWrite.CreateSaveScopeAsync(); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(TenantId.None); - var nexusModsUserToIntegrationSteam = entityFactory.GetOrCreateNexusModsUserSteam(nexusModsUserId, steamUserId); + var nexusModsUserToIntegrationSteam = new NexusModsUserToIntegrationSteamEntity + { + NexusModsUserId = nexusModsUserId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(nexusModsUserId), + SteamUserId = steamUserId, + }; var list = ImmutableArray.CreateBuilder(); foreach (var (tenant, gameIds) in TenantToGameIds) @@ -63,7 +62,7 @@ public async Task CheckOwnedGamesAsync(NexusModsUserId nexusModsUserId, st if (ownsTenant) { - list.Add(new IntegrationSteamToOwnedTenantEntity() + list.Add(new IntegrationSteamToOwnedTenantEntity { SteamUserId = steamUserId, OwnedTenant = tenant, @@ -71,37 +70,56 @@ public async Task CheckOwnedGamesAsync(NexusModsUserId nexusModsUserId, st } } - await _dbContextWrite.NexusModsUserToSteam.UpsertOnSaveAsync(nexusModsUserToIntegrationSteam); - await _dbContextWrite.IntegrationSteamToOwnedTenants.UpsertOnSaveAsync(list.ToArray()); - await _dbContextWrite.IntegrationSteamToOwnedTenants.Where(x => x.SteamUserId == steamUserId).ExecuteDeleteAsync(CancellationToken.None); + unitOfWrite.NexusModsUserToSteam.Upsert(nexusModsUserToIntegrationSteam); + unitOfWrite.IntegrationSteamToOwnedTenants.UpsertRange(list.ToArray()); + unitOfWrite.IntegrationSteamToOwnedTenants.Remove(x => x.SteamUserId == steamUserId); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return true; } public async Task?> GetAsync(string steamUserId) { - var entity = await _dbContextRead.IntegrationSteamTokens.FirstOrDefaultAsync(x => x.SteamUserId.Equals(steamUserId)); + await using var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(TenantId.None); + + var entity = await unitOfRead.IntegrationSteamTokens.FirstOrDefaultAsync(x => x.SteamUserId.Equals(steamUserId), null, CancellationToken.None); if (entity is null) return null; return entity.Data; } public async Task UpsertAsync(NexusModsUserId nexusModsUserId, string steamUserId, Dictionary data) { - var entityFactory = _dbContextWrite.GetEntityFactory(); - await using var _ = await _dbContextWrite.CreateSaveScopeAsync(); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(TenantId.None); - var nexusModsUserToIntegrationSteam = entityFactory.GetOrCreateNexusModsUserSteam(nexusModsUserId, steamUserId); - var tokensSteam = entityFactory.GetOrCreateIntegrationSteamTokens(nexusModsUserId, steamUserId, data); + var nexusModsUserToIntegrationSteam = new NexusModsUserToIntegrationSteamEntity + { + NexusModsUserId = nexusModsUserId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(nexusModsUserId), + SteamUserId = steamUserId, + }; + var tokensSteam = new IntegrationSteamTokensEntity + { + SteamUserId = steamUserId, + NexusModsUserId = nexusModsUserId, + NexusModsUser = unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsUser(nexusModsUserId), + Data = data, + }; - await _dbContextWrite.NexusModsUserToSteam.UpsertOnSaveAsync(nexusModsUserToIntegrationSteam); - await _dbContextWrite.IntegrationSteamTokens.UpsertOnSaveAsync(tokensSteam); + unitOfWrite.NexusModsUserToSteam.Upsert(nexusModsUserToIntegrationSteam); + unitOfWrite.IntegrationSteamTokens.Upsert(tokensSteam); + + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return true; } public async Task RemoveAsync(NexusModsUserId nexusModsUserId, string steamUserId) { - await _dbContextWrite.NexusModsUserToSteam.Where(x => x.NexusModsUser.NexusModsUserId == nexusModsUserId && x.SteamUserId == steamUserId).ExecuteDeleteAsync(); - await _dbContextWrite.IntegrationSteamTokens.Where(x => x.SteamUserId == steamUserId).ExecuteDeleteAsync(); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(TenantId.None); + + unitOfWrite.NexusModsUserToSteam.Remove(x => x.NexusModsUser.NexusModsUserId == nexusModsUserId && x.SteamUserId == steamUserId); + unitOfWrite.IntegrationSteamTokens.Remove(x => x.SteamUserId == steamUserId); + await unitOfWrite.SaveChangesAsync(CancellationToken.None); return true; } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Services/General/ICrashReportBatchedHandler.cs b/src/BUTR.Site.NexusMods.Server/Services/General/ICrashReportBatchedHandler.cs index f92b6c2c..1d3bac02 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/General/ICrashReportBatchedHandler.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/General/ICrashReportBatchedHandler.cs @@ -6,6 +6,7 @@ using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Models.Database; using BUTR.Site.NexusMods.Server.Options; +using BUTR.Site.NexusMods.Server.Repositories; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -26,7 +27,7 @@ public interface ICrashReportBatchedHandler : IAsyncDisposable Task HandleBatchAsync(IEnumerable requests, CancellationToken ct); } -[TransientService] +[ScopedService] public sealed class CrashReportBatchedHandler : ICrashReportBatchedHandler { private record HttpResultEntry(CrashReportFileId FileId, DateTime Date, CrashReportModel? CrashReport); @@ -53,17 +54,17 @@ private static string GetException(ExceptionModel? exception, bool inner = false private readonly ILogger _logger; private readonly CrashReporterOptions _options; - private readonly ITenantContextAccessor _tenantContextAccessor; - private readonly IAppDbContextFactory _dbContextFactory; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; private readonly ICrashReporterClient _client; + private readonly ITenantContextAccessor _tenantContextAccessor; - public CrashReportBatchedHandler(ILogger logger, IOptions options, ITenantContextAccessor tenantContextAccessor, IAppDbContextFactory dbContextFactory, ICrashReporterClient client) + public CrashReportBatchedHandler(ILogger logger, IOptions options, IUnitOfWorkFactory unitOfWorkFactory, ICrashReporterClient client, ITenantContextAccessor tenantContextAccessor) { _logger = logger; _options = options.Value; - _tenantContextAccessor = tenantContextAccessor; - _dbContextFactory = dbContextFactory; + _unitOfWorkFactory = unitOfWorkFactory; _client = client; + _tenantContextAccessor = tenantContextAccessor; } public async Task HandleBatchAsync(IEnumerable requests, CancellationToken ct) @@ -95,18 +96,19 @@ public async Task HandleBatchAsync(IEnumerable req private async Task FilterCrashReportsAsync(IEnumerable crashReports, CancellationToken ct) { + var tenant = _tenantContextAccessor.Current; + try { - var tenant = _tenantContextAccessor.Current; - var dbContextRead = await _dbContextFactory.CreateReadAsync(ct); + var unitOfRead = _unitOfWorkFactory.CreateUnitOfRead(); var uniqueCrashReports = crashReports.DistinctBy(x => x.CrashReportId).ToArray(); var uniqueCrashReportIds = uniqueCrashReports.Select(x => x.CrashReportId).ToArray(); - var existingCrashReportIds = dbContextRead.CrashReports.Where(x => uniqueCrashReportIds.Contains(x.CrashReportId)).Select(x => x.CrashReportId).Distinct().ToArray(); + var existingCrashReportIds = await unitOfRead.CrashReports.GetAllAsync(x => uniqueCrashReportIds.Contains(x.CrashReportId), null, x => x.CrashReportId, ct); var missingCrashReportIds = uniqueCrashReports.ExceptBy(existingCrashReportIds, x => x.CrashReportId).Select(x => x.CrashReportId).ToHashSet(); - var existingLinks = dbContextRead.CrashReportToFileIds.Where(x => existingCrashReportIds.Contains(x.CrashReportId)).Select(x => x.CrashReportId).Distinct().ToArray(); + var existingLinks = await unitOfRead.CrashReportToFileIds.GetAllAsync(x => existingCrashReportIds.Contains(x.CrashReportId), null, x => x.CrashReportId, ct); var missingLinks = uniqueCrashReports.ExceptBy(existingLinks, x => x.CrashReportId).ToArray(); foreach (var missingLink in missingLinks) { @@ -119,10 +121,10 @@ await _linkedCrashReportsChannel.Writer.WriteAsync(new CrashReportToFileIdEntity }, ct); } - var duplicateLinks = dbContextRead.CrashReportToFileIds - .Where(x => uniqueCrashReportIds.Contains(x.CrashReportId)) - .Select(x => new { x.CrashReportId, x.FileId }) - .AsEnumerable() + var uniqueCrashReportsTuple = await unitOfRead.CrashReportToFileIds + .GetAllAsync(x => uniqueCrashReportIds.Contains(x.CrashReportId), null, x => new { x.CrashReportId, x.FileId }, ct); + + var duplicateLinks = uniqueCrashReportsTuple .Join(uniqueCrashReports, x => new { x.CrashReportId }, x => new { x.CrashReportId }, (x, y) => new { y.CrashReportId, DbFileId = x.FileId, DlFileId = y.FileId }) .Where(x => x.DbFileId != x.DlFileId) .Select(x => new { x.CrashReportId, FileId = x.DlFileId }) @@ -133,7 +135,7 @@ await _linkedCrashReportsChannel.Writer.WriteAsync(new CrashReportToFileIdEntity await _ignoredCrashReportsChannel.Writer.WriteAsync(new CrashReportIgnoredFileEntity { TenantId = tenant, - Value = duplicateLink.FileId + CrashReportFileId = duplicateLink.FileId }, ct); } @@ -210,8 +212,7 @@ private async Task WriteCrashReportsToDatabaseAsync(CancellationToken ct) { var tenant = _tenantContextAccessor.Current; - var dbContextWrite = await _dbContextFactory.CreateWriteAsync(ct); - var entityFactory = dbContextWrite.GetEntityFactory(); + await using var unitOfWrite = _unitOfWorkFactory.CreateUnitOfWrite(); var uniqueCrashReportIds = new HashSet(); @@ -237,13 +238,14 @@ private async Task WriteCrashReportsToDatabaseAsync(CancellationToken ct) ignoredCrashReportFileEntities.Add(new CrashReportIgnoredFileEntity { TenantId = tenant, - Value = fileId + CrashReportFileId = fileId }); continue; } uniqueCrashReportIds.Add(crashReportId); + // TODO: var butrLoaderVersion = report.Metadata.AdditionalMetadata.FirstOrDefault(x => x.Key == "BUTRLoaderVersion")?.Value; var blseVersion = report.Metadata.AdditionalMetadata.FirstOrDefault(x => x.Key == "BLSEVersion")?.Value; var launcherExVersion = report.Metadata.AdditionalMetadata.FirstOrDefault(x => x.Key == "LauncherExVersion")?.Value; @@ -255,7 +257,8 @@ private async Task WriteCrashReportsToDatabaseAsync(CancellationToken ct) Url = CrashReportUrl.From(new Uri(new Uri(_options.Endpoint), fileId.ToString())), Version = CrashReportVersion.From(report.Version), GameVersion = GameVersion.From(report.Metadata.GameVersion), - ExceptionType = entityFactory.GetOrCreateExceptionType(ExceptionTypeId.FromException(report.Exception)), + ExceptionTypeId = ExceptionTypeId.FromException(report.Exception), + ExceptionType = unitOfWrite.UpsertEntityFactory.GetOrCreateExceptionType(ExceptionTypeId.FromException(report.Exception)), Exception = GetException(report.Exception), CreatedAt = fileId.Value.Length == 8 ? DateTimeOffset.UnixEpoch.ToUniversalTime() : date.ToUniversalTime(), }); @@ -270,16 +273,18 @@ private async Task WriteCrashReportsToDatabaseAsync(CancellationToken ct) BLSEVersion = blseVersion, LauncherExVersion = launcherExVersion, }); - crashReportModulesBuilder.AddRange(report.Modules.Select((x, i) => new CrashReportToModuleMetadataEntity + crashReportModulesBuilder.AddRange(report.Modules.DistinctBy(x => new { x.Id }).Select(x => new CrashReportToModuleMetadataEntity { TenantId = tenant, CrashReportId = crashReportId, - Module = entityFactory.GetOrCreateModule(ModuleId.From(x.Id)), + ModuleId = ModuleId.From(x.Id), + Module = unitOfWrite.UpsertEntityFactory.GetOrCreateModule(ModuleId.From(x.Id)), Version = ModuleVersion.From(x.Version), - NexusModsMod = NexusModsModId.TryParseUrl(x.Url, out var modId) ? entityFactory.GetOrCreateNexusModsMod(modId) : null, + NexusModsModId = NexusModsModId.TryParseUrl(x.Url, out var modId1) ? modId1 : null, + NexusModsMod = NexusModsModId.TryParseUrl(x.Url, out var modId2) ? unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modId2) : null, InvolvedPosition = (byte) (report.InvolvedModules.IndexOf(y => y.ModuleOrLoaderPluginId == x.Id) + 1), IsInvolved = report.InvolvedModules.Any(y => y.ModuleOrLoaderPluginId == x.Id), - })); + }).ToArray()); } var linkedCrashReports = _linkedCrashReportsChannel.Reader.ReadAllAsync(ct) @@ -289,17 +294,16 @@ private async Task WriteCrashReportsToDatabaseAsync(CancellationToken ct) .Concat(failedCrashReportFileIds.Select(x => new CrashReportIgnoredFileEntity { TenantId = tenant, - Value = x, + CrashReportFileId = x, }).ToAsyncEnumerable()); - await using var _ = await dbContextWrite.CreateSaveScopeAsync(); - await dbContextWrite.CrashReports.UpsertOnSaveAsync(crashReportsBuilder); - await dbContextWrite.CrashReportModuleInfos.UpsertOnSaveAsync(crashReportModulesBuilder); - await dbContextWrite.CrashReportToMetadatas.UpsertOnSaveAsync(crashReportMetadatasBuilder); - await dbContextWrite.CrashReportToFileIds.UpsertOnSaveAsync(linkedCrashReports); - await dbContextWrite.CrashReportIgnoredFileIds.UpsertOnSaveAsync(ignoredCrashReports); - // Disposing the DBContext will save the data + unitOfWrite.CrashReports.UpsertRange(crashReportsBuilder); + unitOfWrite.CrashReportModuleInfos.UpsertRange(crashReportModulesBuilder); + unitOfWrite.CrashReportToMetadatas.UpsertRange(crashReportMetadatasBuilder); + unitOfWrite.CrashReportToFileIds.UpsertRange(await linkedCrashReports.ToArrayAsync(ct)); + unitOfWrite.CrashReportIgnoredFileIds.UpsertRange(await ignoredCrashReports.ToArrayAsync(ct)); + await unitOfWrite.SaveChangesAsync(ct); return crashReportsBuilder.Count; } diff --git a/src/BUTR.Site.NexusMods.Server/Services/General/INexusModsModFileParser.cs b/src/BUTR.Site.NexusMods.Server/Services/General/INexusModsModFileParser.cs index 6ff3ef3a..dbe8dc29 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/General/INexusModsModFileParser.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/General/INexusModsModFileParser.cs @@ -93,8 +93,8 @@ public async IAsyncEnumerable GetModuleInfosAsync( if (reader is null) throw new InvalidOperationException($"Failed to get Reader for file '{fileInfo.FileName}'"); var moduleInfosReader = await GetModuleInfosFromReaderAsync(reader, subModuleCount).ToListAsync(ct); - var dataReader = await GetGameVersionsFromReaderAsync(reader, moduleInfosReader).ToListAsync(ct); - foreach (var grouping in dataReader.GroupBy(x => new { x.Item1.Id, x.Item1.Version })) + var gameVersions = await GetGameVersionsFromReaderAsync(reader, moduleInfosReader).ToListAsync(ct); + foreach (var grouping in gameVersions.GroupBy(x => new { x.Item1.Id, x.Item1.Version })) { yield return new() { @@ -132,7 +132,7 @@ private static async IAsyncEnumerable GetModuleInfos { while (subModuleCount > 0 && reader.MoveToNextEntry()) { - if (reader.Entry.IsDirectory) continue; + if (reader.Entry.IsDirectory || reader.Entry.Key is null) continue; if (!reader.Entry.Key.Contains("SubModule.xml", StringComparison.OrdinalIgnoreCase)) continue; @@ -150,7 +150,7 @@ private static async IAsyncEnumerable GetModuleInfos { if (subModuleCount <= 0) break; - if (entry.IsDirectory) continue; + if (entry.IsDirectory || entry.Key is null) continue; if (!entry.Key.Contains("SubModule.xml", StringComparison.OrdinalIgnoreCase)) continue; @@ -171,10 +171,10 @@ private static async IAsyncEnumerable GetModuleInfos } yield break; - var count = moduleInfos.SelectMany(x => x.SubModules).Select(x => x.DLLName).Count(); - while (count > 0 && reader.MoveToNextEntry()) + var subModuleCount = moduleInfos.SelectMany(x => x.SubModules).Select(x => x.DLLName).Count(); + while (subModuleCount > 0 && reader.MoveToNextEntry()) { - if (reader.Entry.IsDirectory) continue; + if (reader.Entry.IsDirectory || reader.Entry.Key is null) continue; if (!reader.Entry.Key.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) continue; @@ -193,7 +193,7 @@ private static async IAsyncEnumerable GetModuleInfos var assembly = AssemblyDefinition.FromImage(PEImage.FromDataSource(new StreamDataSource(ms))); foreach (var gameVersion in GetGameVersions(assembly)) yield return (moduleInfo, subModule, gameVersion); - count--; + subModuleCount--; } } } diff --git a/src/BUTR.Site.NexusMods.Server/Services/General/ISteamDepotDownloader.cs b/src/BUTR.Site.NexusMods.Server/Services/General/ISteamDepotDownloader.cs index e2a624f6..eaa6988e 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/General/ISteamDepotDownloader.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/General/ISteamDepotDownloader.cs @@ -22,7 +22,7 @@ public sealed class SteamDepotDownloader : ISteamDepotDownloader public SteamDepotDownloader(IOptions options) { - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); + _options = options.Value; } public async Task DownloadAsync(string version, string path, CancellationToken ct) diff --git a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/ICrashReporterClient.cs b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/ICrashReporterClient.cs index 0313ea3d..e2d7eade 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/ICrashReporterClient.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/ICrashReporterClient.cs @@ -29,8 +29,8 @@ public sealed class CrashReporterClient : ICrashReporterClient public CrashReporterClient(HttpClient httpClient, IOptions jsonSerializerOptions) { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - _jsonSerializerOptions = jsonSerializerOptions.Value ?? throw new ArgumentNullException(nameof(jsonSerializerOptions)); + _httpClient = httpClient; + _jsonSerializerOptions = jsonSerializerOptions.Value; } public async Task GetCrashReportAsync(CrashReportFileId id, CancellationToken ct) => await _httpClient.GetStringAsync($"{id}.html", ct); @@ -44,14 +44,16 @@ public CrashReporterClient(HttpClient httpClient, IOptions GetNewCrashReportMetadatasAsync(DateTime dateTime, [EnumeratorCancellation] CancellationToken ct) { - using var request = new HttpRequestMessage(HttpMethod.Post, "getnewcrashreports") { Content = JsonContent.Create(new { DateTime = dateTime.ToString("o") }, options: _jsonSerializerOptions) }; + using var request = new HttpRequestMessage(HttpMethod.Post, "getnewcrashreports"); + request.Content = JsonContent.Create(new { DateTime = dateTime.ToString("o") }, options: _jsonSerializerOptions); using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct); await foreach (var entry in JsonSerializer.DeserializeAsyncEnumerable(await response.Content.ReadAsStreamAsync(ct), _jsonSerializerOptions, ct)) yield return entry; } public async IAsyncEnumerable GetCrashReportMetadatasAsync(IEnumerable filenames, [EnumeratorCancellation] CancellationToken ct) { - using var request = new HttpRequestMessage(HttpMethod.Post, "getmetadata") { Content = JsonContent.Create(filenames, options: _jsonSerializerOptions) }; + using var request = new HttpRequestMessage(HttpMethod.Post, "getmetadata"); + request.Content = JsonContent.Create(filenames, options: _jsonSerializerOptions); using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct); await foreach (var entry in JsonSerializer.DeserializeAsyncEnumerable(await response.Content.ReadAsStreamAsync(ct), _jsonSerializerOptions, ct)) yield return entry; diff --git a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IDiscordClient.cs b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IDiscordClient.cs index 6cc5e701..480a426a 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IDiscordClient.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IDiscordClient.cs @@ -68,20 +68,15 @@ public sealed record DiscordOAuthTokensResponse( public DiscordClient(HttpClient httpClient, IOptions options) { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); + _httpClient = httpClient; + _options = options.Value; } public async Task SetGlobalMetadataAsync(IReadOnlyList metadata, CancellationToken ct) { - using var request = new HttpRequestMessage(HttpMethod.Put, $"v10/applications/{_options.ClientId}/role-connections/metadata") - { - Headers = - { - Authorization = new AuthenticationHeaderValue("Bot", _options.BotToken), - }, - Content = new StringContent(JsonSerializer.Serialize(metadata), Encoding.UTF8, "application/json"), - }; + using var request = new HttpRequestMessage(HttpMethod.Put, $"v10/applications/{_options.ClientId}/role-connections/metadata"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bot", _options.BotToken); + request.Content = new StringContent(JsonSerializer.Serialize(metadata), Encoding.UTF8, "application/json"); using var response = await _httpClient.SendAsync(request, ct); return response.IsSuccessStatusCode; } @@ -104,17 +99,15 @@ public async Task SetGlobalMetadataAsync(IReadOnlyList CreateTokensAsync(string code, CancellationToken ct) { - using var request = new HttpRequestMessage(HttpMethod.Post, "v10/oauth2/token") + using var request = new HttpRequestMessage(HttpMethod.Post, "v10/oauth2/token"); + request.Content = new FormUrlEncodedContent(new List> { - Content = new FormUrlEncodedContent(new List> - { - new("client_id", _options.ClientId), - new("client_secret", _options.ClientSecret), - new("redirect_uri", _options.RedirectUri), - new("grant_type", "authorization_code"), - new("code", code), - }) - }; + new("client_id", _options.ClientId), + new("client_secret", _options.ClientSecret), + new("redirect_uri", _options.RedirectUri), + new("grant_type", "authorization_code"), + new("code", code), + }); using var response = await _httpClient.SendAsync(request, ct); if (!response.IsSuccessStatusCode) return null; var tokens = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(ct), cancellationToken: ct); @@ -146,13 +139,8 @@ public async Task SetGlobalMetadataAsync(IReadOnlyList GetUserInfoAsync(DiscordOAuthTokens tokens, CancellationToken ct) { - using var request = new HttpRequestMessage(HttpMethod.Get, "v10/oauth2/@me") - { - Headers = - { - Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken) - } - }; + using var request = new HttpRequestMessage(HttpMethod.Get, "v10/oauth2/@me"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); using var response = await _httpClient.SendAsync(request, ct); if (!response.IsSuccessStatusCode) return null; return await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(ct), cancellationToken: ct); @@ -160,14 +148,9 @@ public async Task SetGlobalMetadataAsync(IReadOnlyList PushMetadataAsync(DiscordOAuthTokens tokens, T metadata, CancellationToken ct) { - using var request = new HttpRequestMessage(HttpMethod.Put, $"v10/users/@me/applications/{_options.ClientId}/role-connection") - { - Headers = - { - Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken) - }, - Content = JsonContent.Create(new PutMetadata("BUTR", metadata)), - }; + using var request = new HttpRequestMessage(HttpMethod.Put, $"v10/users/@me/applications/{_options.ClientId}/role-connection"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); + request.Content = JsonContent.Create(new PutMetadata("BUTR", metadata)); using var response = await _httpClient.SendAsync(request, ct); return response.IsSuccessStatusCode; } diff --git a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGOGAuthClient.cs b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGOGAuthClient.cs index 2dc02ac4..196ac43e 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGOGAuthClient.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGOGAuthClient.cs @@ -37,7 +37,7 @@ public record TokenResponse( public GOGAuthClient(HttpClient httpClient) { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + _httpClient = httpClient; } public string GetOAuth2Url() => OAuth2Url; diff --git a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGOGEmbedClient.cs b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGOGEmbedClient.cs index 449e56b4..ec29f5f5 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGOGEmbedClient.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGOGEmbedClient.cs @@ -37,20 +37,15 @@ public record UserInfo( public GOGEmbedClient(HttpClient httpClient, IOptions jsonSerializerOptions) { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - _jsonSerializerOptions = jsonSerializerOptions.Value ?? throw new ArgumentNullException(nameof(httpClient)); + _httpClient = httpClient; + _jsonSerializerOptions = jsonSerializerOptions.Value; } public async Task GetUserInfoAsync(string token, CancellationToken ct) { - using var request = new HttpRequestMessage(HttpMethod.Get, "/userData.json") - { - Headers = - { - Authorization = new AuthenticationHeaderValue("Bearer", token), - Accept = { new MediaTypeWithQualityHeaderValue("application/json") } - } - }; + using var request = new HttpRequestMessage(HttpMethod.Get, "/userData.json"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); using var response = await _httpClient.SendAsync(request, ct); if (!response.IsSuccessStatusCode) return null; @@ -61,14 +56,9 @@ public GOGEmbedClient(HttpClient httpClient, IOptions jso public async Task GetGamesAsync(string token, CancellationToken ct) { - using var request = new HttpRequestMessage(HttpMethod.Get, "/user/data/games") - { - Headers = - { - Authorization = new AuthenticationHeaderValue("Bearer", token), - Accept = { new MediaTypeWithQualityHeaderValue("application/json") } - } - }; + using var request = new HttpRequestMessage(HttpMethod.Get, "/user/data/games"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); using var response = await _httpClient.SendAsync(request, ct); if (!response.IsSuccessStatusCode) return null; diff --git a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGitHubAPIClient.cs b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGitHubAPIClient.cs index 7ca50f9e..1bbf7041 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGitHubAPIClient.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGitHubAPIClient.cs @@ -27,19 +27,14 @@ public sealed class GitHubAPIClient : IGitHubAPIClient public GitHubAPIClient(HttpClient httpClient, IOptions jsonSerializerOptions) { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - _jsonSerializerOptions = jsonSerializerOptions.Value ?? throw new ArgumentNullException(nameof(httpClient)); + _httpClient = httpClient; + _jsonSerializerOptions = jsonSerializerOptions.Value; } public async Task GetUserInfoAsync(GitHubOAuthTokens tokens, CancellationToken ct) { - using var request = new HttpRequestMessage(HttpMethod.Get, "user") - { - Headers = - { - Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken) - } - }; + using var request = new HttpRequestMessage(HttpMethod.Get, "user"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); using var response = await _httpClient.SendAsync(request, ct); if (!response.IsSuccessStatusCode) return null; return await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(ct), _jsonSerializerOptions, cancellationToken: ct); diff --git a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGitHubClient.cs b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGitHubClient.cs index bdecd2c5..011e94fd 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGitHubClient.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/IGitHubClient.cs @@ -32,8 +32,8 @@ public sealed record GitHubOAuthTokensResponse( public GitHubClient(HttpClient httpClient, IOptions options) { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); + _httpClient = httpClient; + _options = options.Value; } public (string Url, Guid State) GetOAuthUrl() @@ -51,20 +51,15 @@ public GitHubClient(HttpClient httpClient, IOptions options) public async Task CreateTokensAsync(string code, CancellationToken ct) { - using var request = new HttpRequestMessage(HttpMethod.Post, "login/oauth/access_token") + using var request = new HttpRequestMessage(HttpMethod.Post, "login/oauth/access_token"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Content = new FormUrlEncodedContent(new List> { - Headers = - { - Accept = { new MediaTypeWithQualityHeaderValue("application/json") } - }, - Content = new FormUrlEncodedContent(new List> - { - new("client_id", _options.ClientId), - new("client_secret", _options.ClientSecret), - new("redirect_uri", _options.RedirectUri), - new("code", code), - }) - }; + new("client_id", _options.ClientId), + new("client_secret", _options.ClientSecret), + new("redirect_uri", _options.RedirectUri), + new("code", code), + }); using var response = await _httpClient.SendAsync(request, ct); if (!response.IsSuccessStatusCode) return null; var tokens = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(ct), cancellationToken: ct); diff --git a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/INexusModsAPIClient.cs b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/INexusModsAPIClient.cs index 0192da6d..7f8464ea 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/INexusModsAPIClient.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/INexusModsAPIClient.cs @@ -44,9 +44,9 @@ public sealed class NexusModsAPIClient : INexusModsAPIClient public NexusModsAPIClient(HttpClient httpClient, IDistributedCache cache, IOptions jsonSerializerOptions) { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - _cache = cache ?? throw new ArgumentNullException(nameof(cache)); - _jsonSerializerOptions = jsonSerializerOptions.Value ?? throw new ArgumentNullException(nameof(jsonSerializerOptions)); + _httpClient = httpClient; + _cache = cache; + _jsonSerializerOptions = jsonSerializerOptions.Value; } private static string HashString(string value) diff --git a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/INexusModsAPIv2Client.cs b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/INexusModsAPIv2Client.cs new file mode 100644 index 00000000..ae796d8c --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/INexusModsAPIv2Client.cs @@ -0,0 +1,89 @@ +using BUTR.Site.NexusMods.Server.Models; + +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; + +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace BUTR.Site.NexusMods.Server.Services; + +public interface INexusModsAPIv2Client +{ + Task GetUserIdAsync(NexusModsApiKey apiKey, NexusModsUserName username, CancellationToken ct); +} + +public sealed class NexusModsAPIv2Client : INexusModsAPIv2Client +{ + private record GraphQLQuery + { + public string Query { get; init; } + } + + private record GraphQLResponse(TData Data); + private record GraphQLGetUserByNameResponse + { + public string MemberId { get; init; } + } + + + private readonly HttpClient _httpClient; + private readonly IDistributedCache _cache; + private readonly JsonSerializerOptions _jsonSerializerOptions; + private readonly DistributedCacheEntryOptions _expiration = new() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }; + + public NexusModsAPIv2Client(HttpClient httpClient, IDistributedCache cache, IOptions jsonSerializerOptions) + { + _httpClient = httpClient; + _cache = cache; + _jsonSerializerOptions = jsonSerializerOptions.Value; + } + + private static string HashString(string value) + { + Span data2 = stackalloc byte[Encoding.UTF8.GetByteCount(value)]; + Encoding.UTF8.GetBytes(value, data2); + Span data = stackalloc byte[64]; + SHA512.HashData(data2, data); + return Convert.ToBase64String(data); + } + + public async Task GetUserIdAsync(NexusModsApiKey apiKey, NexusModsUserName username, CancellationToken ct) + { + var key = HashString($"Username:{username}"); + try + { + if (await _cache.GetStringAsync(key, token: ct) is { } raw) + return string.IsNullOrEmpty(raw) ? NexusModsUserId.None : NexusModsUserId.Parse(raw); + + using var request = new HttpRequestMessage(HttpMethod.Post, "v2/graphql"); + request.Headers.Add("apikey", apiKey.Value); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Content = new StringContent(JsonSerializer.Serialize(new GraphQLQuery + { + Query = "{ userByName(name: \"Aragas\") { memberId } }" + }), Encoding.UTF8, "application/json"); + using var response = await _httpClient.SendAsync(request, ct); + if (!response.IsSuccessStatusCode) return NexusModsUserId.None; + + var json = await response.Content.ReadAsStringAsync(ct); + var responseJson = JsonSerializer.Deserialize>(json, _jsonSerializerOptions); + if (responseJson?.Data is null || !NexusModsUserId.TryParse(responseJson.Data.MemberId, out var userId)) return NexusModsUserId.None; + + raw = responseJson.Data.MemberId; + await _cache.SetStringAsync(key, raw, _expiration, token: ct); + return userId; + } + catch (Exception) + { + await _cache.RemoveAsync(key, ct); + return NexusModsUserId.None; + } + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/INexusModsClient.cs b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/INexusModsClient.cs index aeda427a..5b5e2c0b 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/INexusModsClient.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/INexusModsClient.cs @@ -20,7 +20,7 @@ public sealed class NexusModsClient : INexusModsClient public NexusModsClient(HttpClient httpClient) { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + _httpClient = httpClient; } public async Task GetArticleAsync(NexusModsGameDomain gameDomain, NexusModsArticleId articleId, CancellationToken ct) diff --git a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/INexusModsUsersClient.cs b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/INexusModsUsersClient.cs new file mode 100644 index 00000000..9397f661 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/INexusModsUsersClient.cs @@ -0,0 +1,112 @@ +using BUTR.Site.NexusMods.Server.Models; +using BUTR.Site.NexusMods.Server.Options; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using System.Web; + +namespace BUTR.Site.NexusMods.Server.Services; + +public sealed record NexusModsOAuthTokens(string AccessToken, string? RefreshToken); + +public sealed record NexusModsUserInfo( + [property: JsonPropertyName("sub")] string UserId, + [property: JsonPropertyName("name")] NexusModsUserName Name, + [property: JsonPropertyName("email")] NexusModsUserEMail Email, + [property: JsonPropertyName("avatar")] string? AvatarUrl, + [property: JsonPropertyName("membership_roles")] string[] MembershipRoles); + +public interface INexusModsUsersClient +{ + (string Url, string CodeVerifier, Guid State) GetOAuthUrl(); + Task CreateTokensAsync(string code, string codeVerifier, CancellationToken ct); + Task GetUserInfoAsync(NexusModsOAuthTokens tokens, CancellationToken ct); +} + +public sealed class NexusModsUsersClient : INexusModsUsersClient +{ + public sealed record NexusModsOAuthTokensResponse( + [property: JsonPropertyName("access_token")] string AccessToken, + [property: JsonPropertyName("_received_at")] long ReceivedAt, + [property: JsonPropertyName("token_type")] string? Type, + [property: JsonPropertyName("expires_in")] ulong ExpiresIn, + [property: JsonPropertyName("refresh_token")] string? RefreshToken, + [property: JsonPropertyName("scope")] string? Scope, + [property: JsonPropertyName("created_at")] long CreatedAt) + { + public bool IsExpired => DateTime.FromFileTimeUtc(ReceivedAt) + TimeSpan.FromSeconds(ExpiresIn) - TimeSpan.FromMinutes(5) <= DateTimeOffset.UtcNow; + + } + + private readonly HttpClient _httpClient; + private readonly NexusModsUsersOptions _options; + + public NexusModsUsersClient(HttpClient httpClient, IOptions options) + { + _httpClient = httpClient; + _options = options.Value; + } + + public (string Url, string CodeVerifier, Guid State) GetOAuthUrl() + { + var state = Guid.NewGuid(); + + // see https://www.rfc-editor.org/rfc/rfc7636#section-4.1 + var codeVerifier = Convert.ToBase64String(Encoding.ASCII.GetBytes(Guid.NewGuid().ToString("N"))); + + // see https://www.rfc-editor.org/rfc/rfc7636#section-4.2 + var codeChallengeBytes = SHA256.HashData(Encoding.ASCII.GetBytes(codeVerifier)); + var codeChallenge = Base64UrlTextEncoder.Encode(codeChallengeBytes); + + var url = new UriBuilder($"{_httpClient.BaseAddress}oauth/authorize"); + var query = HttpUtility.ParseQueryString(url.Query); + query["response_type"] = "code"; + query["scope"] = "openid profile email"; + query["code_challenge_method"] = "S256"; + query["redirect_uri"] = _options.RedirectUri; + query["code_challenge"] = codeChallenge; + query["state"] = state.ToString(); + query["client_id"] = _options.ClientId; + url.Query = query.ToString(); + return (url.ToString(), codeVerifier, state); + } + + public async Task CreateTokensAsync(string code, string codeVerifier, CancellationToken ct) + { + using var request = new HttpRequestMessage(HttpMethod.Post, "oauth/token"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Content = new FormUrlEncodedContent(new List> + { + new("grant_type", "authorization_code"), + new("client_id", _options.ClientId), + new("redirect_uri", _options.RedirectUri), + new("code", code), + new("code_verifier", codeVerifier), + }); + using var response = await _httpClient.SendAsync(request, ct); + if (!response.IsSuccessStatusCode) return null; + var tokens = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(ct), cancellationToken: ct); + return tokens is not null ? new NexusModsOAuthTokens(tokens.AccessToken, tokens.RefreshToken) : null; + } + + public async Task GetUserInfoAsync(NexusModsOAuthTokens tokens, CancellationToken ct) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "oauth/userinfo"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); + using var response = await _httpClient.SendAsync(request, ct); + if (!response.IsSuccessStatusCode) return null; + return await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(ct), cancellationToken: ct); + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/ISteamAPIClient.cs b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/ISteamAPIClient.cs index 5b8c4c8c..bade542d 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/ISteamAPIClient.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/ISteamAPIClient.cs @@ -74,8 +74,8 @@ public record IsOwningGameGame( public SteamAPIClient(HttpClient httpClient, IOptions options) { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); + _httpClient = httpClient; + _options = options.Value; } public async Task GetUserInfoAsync(string steamId, CancellationToken ct) diff --git a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/ISteamCommunityClient.cs b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/ISteamCommunityClient.cs index 70fa14aa..0850016e 100644 --- a/src/BUTR.Site.NexusMods.Server/Services/HttpClients/ISteamCommunityClient.cs +++ b/src/BUTR.Site.NexusMods.Server/Services/HttpClients/ISteamCommunityClient.cs @@ -17,7 +17,7 @@ public sealed class SteamCommunityClient : ISteamCommunityClient public SteamCommunityClient(HttpClient httpClient) { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + _httpClient = httpClient; } public async Task ConfirmIdentityAsync(Dictionary parameters, CancellationToken ct) @@ -30,10 +30,8 @@ public async Task ConfirmIdentityAsync(Dictionary paramete foreach (var parameter in parameters) query.TryAdd(parameter.Key, parameter.Value); - using var request = new HttpRequestMessage(HttpMethod.Post, "openid/login") - { - Content = new FormUrlEncodedContent(query) - }; + using var request = new HttpRequestMessage(HttpMethod.Post, "openid/login"); + request.Content = new FormUrlEncodedContent(query); using var response = await _httpClient.SendAsync(request, ct); var responseString = await response.Content.ReadAsStringAsync(ct); diff --git a/src/BUTR.Site.NexusMods.Server/Startup.cs b/src/BUTR.Site.NexusMods.Server/Startup.cs index 75f51235..b567152c 100644 --- a/src/BUTR.Site.NexusMods.Server/Startup.cs +++ b/src/BUTR.Site.NexusMods.Server/Startup.cs @@ -8,6 +8,7 @@ using BUTR.Site.NexusMods.Server.Jobs; using BUTR.Site.NexusMods.Server.Models; using BUTR.Site.NexusMods.Server.Options; +using BUTR.Site.NexusMods.Server.Repositories; using BUTR.Site.NexusMods.Server.Services; using BUTR.Site.NexusMods.Server.Utils; using BUTR.Site.NexusMods.Server.Utils.BindingSources; @@ -20,6 +21,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -57,6 +59,7 @@ public sealed partial class Startup private const string ConnectionStringsSectionName = "ConnectionStrings"; private const string CrashReporterSectionName = "CrashReporter"; private const string NexusModsSectionName = "NexusMods"; + private const string NexusModsUsersSectionName = "NexusModsUsers"; private const string JwtSectionName = "Jwt"; private const string GitHubSectionName = "GitHub"; private const string DiscordSectionName = "Discord"; @@ -86,7 +89,7 @@ private static AsyncRetryPolicy GetRetryPolicy() public Startup(IConfiguration configuration) { - _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _configuration = configuration; } partial void ConfigureServicesPartial(IServiceCollection services); @@ -98,6 +101,7 @@ public void ConfigureServices(IServiceCollection services) var connectionStringSection = _configuration.GetSection(ConnectionStringsSectionName); var crashReporterSection = _configuration.GetSection(CrashReporterSectionName); var nexusModsSection = _configuration.GetSection(NexusModsSectionName); + var nexusModsUsersSection = _configuration.GetSection(NexusModsUsersSectionName); var jwtSection = _configuration.GetSection(JwtSectionName); var gitHubSection = _configuration.GetSection(GitHubSectionName); var discordSection = _configuration.GetSection(DiscordSectionName); @@ -108,6 +112,7 @@ public void ConfigureServices(IServiceCollection services) services.AddValidatedOptions().Bind(connectionStringSection); services.AddValidatedOptionsWithHttp().Bind(crashReporterSection); services.AddValidatedOptionsWithHttp().Bind(nexusModsSection); + services.AddValidatedOptionsWithHttp().Bind(nexusModsUsersSection); services.AddValidatedOptions().Bind(jwtSection); services.AddValidatedOptions().Bind(gitHubSection); services.AddValidatedOptions().Bind(discordSection); @@ -128,6 +133,16 @@ public void ConfigureServices(IServiceCollection services) client.BaseAddress = new Uri("https://api.nexusmods.com/"); client.DefaultRequestHeaders.Add("User-Agent", userAgent); }).AddPolicyHandler(GetRetryPolicy()); + services.AddHttpClient().ConfigureHttpClient((_, client) => + { + client.BaseAddress = new Uri("https://api.nexusmods.com/"); + client.DefaultRequestHeaders.Add("User-Agent", userAgent); + }).AddPolicyHandler(GetRetryPolicy()); + services.AddHttpClient().ConfigureHttpClient((_, client) => + { + client.BaseAddress = new Uri("https://users.nexusmods.com/"); + client.DefaultRequestHeaders.Add("User-Agent", userAgent); + }).AddPolicyHandler(GetRetryPolicy()); services.AddHttpClient().ConfigureHttpClient((sp, client) => { var opts = sp.GetRequiredService>().Value; @@ -183,6 +198,8 @@ public void ConfigureServices(IServiceCollection services) string AtEveryDay(int hour = 0) => $"0 0 {hour} ? * *"; string AtEveryHour() => "0 0 * * * ?"; #if DEBUG + //opt.AddJobAtStartup(); + /* opt.AddJob(); opt.AddJob(); opt.AddJob(); @@ -192,6 +209,7 @@ public void ConfigureServices(IServiceCollection services) opt.AddJob(); opt.AddJob(); opt.AddJob(); + */ #else // Hourly opt.AddJob(CronScheduleBuilder.CronSchedule(AtEveryHour()).InTimeZone(TimeZoneInfo.Utc)); @@ -212,6 +230,8 @@ public void ConfigureServices(IServiceCollection services) services.AddMemoryCache(); + + var types = typeof(Startup).Assembly.GetTypes().Where(x => x is { IsAbstract: false, BaseType: { IsGenericType: true } }).ToList(); foreach (var type in types.Where(x => x.BaseType!.GetGenericTypeDefinition() == typeof(BaseEntityConfigurationWithTenant<>))) services.TryAddEnumerable(ServiceDescriptor.Scoped(typeof(IEntityConfiguration), type)); @@ -221,8 +241,6 @@ public void ConfigureServices(IServiceCollection services) services.AddDbContext(ServiceLifetime.Scoped); services.AddDbContextFactory(lifetime: ServiceLifetime.Scoped); services.AddDbContextFactory(lifetime: ServiceLifetime.Scoped); - services.AddScoped(sp => sp.GetRequiredService().CreateRead()); - services.AddScoped(sp => sp.GetRequiredService().CreateWrite()); services.AddNexusModsDefaultServices(); @@ -236,18 +254,24 @@ public void ConfigureServices(IServiceCollection services) services.AddStreamingMultipartResult(); + services.AddHttpContextAccessor(); + services.AddRouting(opt => + { + opt.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); + }); services.AddControllersWithAPIResult(opt => { + opt.Conventions.Add(new SlugifyActionConvention()); + opt.Conventions.Add(new RouteTokenTransformerConvention(new SlugifyParameterTransformer())); + opt.AddCsvOutputFormatters(); + opt.ValueProviderFactories.Add(new ClaimsValueProviderFactory()); }).AddJsonOptions(opt => Configure(opt.JsonSerializerOptions)); - - services.AddHttpContextAccessor(); - services.AddRouting(); - services.AddResponseCompression(options => + services.AddResponseCompression(opt => { - options.Providers.Add(); - options.Providers.Add(); + opt.Providers.Add(); + opt.Providers.Add(); }); services.Configure(options => { @@ -288,13 +312,15 @@ public void ConfigureServices(IServiceCollection services) { jwtSecurityScheme, Array.Empty() } }); + opt.OperationFilter(); opt.DescribeAllParametersInCamelCase(); opt.SupportNonNullableReferenceTypes(); opt.SchemaFilter(); + opt.SchemaFilter(); opt.OperationFilter(); opt.OperationFilter(); opt.OperationFilter(); - opt.ValueObjectFilter(); + opt.SchemaFilter(); opt.EnableAnnotations(); opt.UseAllOfToExtendReferenceSchemas(); @@ -308,7 +334,7 @@ public void ConfigureServices(IServiceCollection services) .Where(File.Exists) .ToList(); foreach (var xmlFilePath in xmlFilePaths) - opt.IncludeXmlComments(xmlFilePath); + opt.IncludeXmlComments(xmlFilePath, true); }); services.Configure(options => diff --git a/src/BUTR.Site.NexusMods.Server/Utils/BindingSources/BindApiKeyAttribute.cs b/src/BUTR.Site.NexusMods.Server/Utils/BindingSources/BindUserNameAttribute.cs similarity index 72% rename from src/BUTR.Site.NexusMods.Server/Utils/BindingSources/BindApiKeyAttribute.cs rename to src/BUTR.Site.NexusMods.Server/Utils/BindingSources/BindUserNameAttribute.cs index 3941c602..969d3656 100644 --- a/src/BUTR.Site.NexusMods.Server/Utils/BindingSources/BindApiKeyAttribute.cs +++ b/src/BUTR.Site.NexusMods.Server/Utils/BindingSources/BindUserNameAttribute.cs @@ -9,10 +9,10 @@ namespace BUTR.Site.NexusMods.Server.Utils.BindingSources; [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] -public class BindApiKeyAttribute : ValidationAttribute, IBindingSourceMetadata, IModelNameProvider, IBindIgnore +public class BindUserNameAttribute : ValidationAttribute, IBindingSourceMetadata, IModelNameProvider, IBindIgnore { public BindingSource BindingSource => ClaimsBindingSource.BindingSource; - public string Name => ButrNexusModsClaimTypes.APIKey; + public string Name => ButrNexusModsClaimTypes.Name; - public override bool IsValid(object? value) => value is NexusModsApiKey; + public override bool IsValid(object? value) => value is NexusModsUserName; } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Utils/Http/ApiResults/ApiResultOperationFilter.cs b/src/BUTR.Site.NexusMods.Server/Utils/Http/ApiResults/ApiResultOperationFilter.cs index b578a479..960d1158 100644 --- a/src/BUTR.Site.NexusMods.Server/Utils/Http/ApiResults/ApiResultOperationFilter.cs +++ b/src/BUTR.Site.NexusMods.Server/Utils/Http/ApiResults/ApiResultOperationFilter.cs @@ -2,10 +2,34 @@ using Swashbuckle.AspNetCore.SwaggerGen; +using System.Reflection; + namespace BUTR.Site.NexusMods.Server.Utils.Http.ApiResults; public sealed class ApiResultOperationFilter : IOperationFilter { + private static T CopyPublicProperties(T oldObject, T newObject) where T : class + { + const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance; + + if (ReferenceEquals(oldObject, newObject)) return newObject; + + var type = typeof(T); + var propertyList = type.GetProperties(flags); + if (propertyList.Length <= 0) return newObject; + + foreach (var newObjProp in propertyList) + { + var oldProp = type.GetProperty(newObjProp.Name, flags)!; + if (!oldProp.CanRead || !newObjProp.CanWrite) continue; + + var value = oldProp.GetValue(oldObject); + newObjProp.SetValue(newObject, value); + } + + return newObject; + } + public void Apply(OpenApiOperation operation, OperationFilterContext? context) { if (context == default || context.MethodInfo == default) @@ -17,11 +41,11 @@ public void Apply(OpenApiOperation operation, OperationFilterContext? context) if (!operation.Responses.TryGetValue("200", out var successResponse)) return; - var copy400 = CopyHelper.CopyPublicProperties(successResponse, new OpenApiResponse()); + var copy400 = CopyPublicProperties(successResponse, new OpenApiResponse()); copy400.Description = "Invalid API Request."; operation.Responses.Add("400", copy400); - var copy500 = CopyHelper.CopyPublicProperties(successResponse, new OpenApiResponse()); + var copy500 = CopyPublicProperties(successResponse, new OpenApiResponse()); copy500.Description = "API Request Execution Error."; operation.Responses.Add("500", copy500); diff --git a/src/BUTR.Site.NexusMods.Server/Utils/Http/ApiResults/ApiResultUtils.cs b/src/BUTR.Site.NexusMods.Server/Utils/Http/ApiResults/ApiResultUtils.cs index 6e15c02d..c57938b8 100644 --- a/src/BUTR.Site.NexusMods.Server/Utils/Http/ApiResults/ApiResultUtils.cs +++ b/src/BUTR.Site.NexusMods.Server/Utils/Http/ApiResults/ApiResultUtils.cs @@ -18,7 +18,7 @@ public static bool IsReturnTypeApiResult(MethodInfo? methodInfo) private static Type GetReturnType(MethodInfo methodInfo) { var returnType = methodInfo.ReturnType; - if (returnType.IsGenericType && IsTaskType(returnType.GetGenericTypeDefinition())) + if (returnType.IsGenericTypeDefinition && IsTaskType(returnType.GetGenericTypeDefinition())) returnType = returnType.GenericTypeArguments[0]; return returnType; diff --git a/src/BUTR.Site.NexusMods.Server/Utils/Http/ApiResults/IServiceCollectionExtensions.cs b/src/BUTR.Site.NexusMods.Server/Utils/Http/ApiResults/IServiceCollectionExtensions.cs index 84d33557..f61f3ace 100644 --- a/src/BUTR.Site.NexusMods.Server/Utils/Http/ApiResults/IServiceCollectionExtensions.cs +++ b/src/BUTR.Site.NexusMods.Server/Utils/Http/ApiResults/IServiceCollectionExtensions.cs @@ -16,7 +16,7 @@ public static IMvcBuilder AddControllersWithAPIResult(this IServiceCollection se services.AddSingleton(); services.AddProblemDetails(); - var builder = services.AddControllers().HandleInvalidModelStateError().AddMvcOptions(configure); + var builder = services.AddControllers(configure).HandleInvalidModelStateError(); services.Decorate(); @@ -35,7 +35,7 @@ private static void Decorate(this IServiceCollection ser services.Replace(ServiceDescriptor.Describe( typeof(TInterface), - serviceProvider => (TInterface) decoratorFactory(serviceProvider, new[] { serviceProvider.CreateInstance(interfaceDescriptor) }), + serviceProvider => (TInterface) decoratorFactory(serviceProvider, [serviceProvider.CreateInstance(interfaceDescriptor)]), interfaceDescriptor.Lifetime)); } diff --git a/src/BUTR.Site.NexusMods.Server/Utils/Http/Logging/SyncLoggingHttpMessageHandler.cs b/src/BUTR.Site.NexusMods.Server/Utils/Http/Logging/SyncLoggingHttpMessageHandler.cs index 5d5c9c5f..ab226cfc 100644 --- a/src/BUTR.Site.NexusMods.Server/Utils/Http/Logging/SyncLoggingHttpMessageHandler.cs +++ b/src/BUTR.Site.NexusMods.Server/Utils/Http/Logging/SyncLoggingHttpMessageHandler.cs @@ -25,7 +25,7 @@ public class SyncLoggingHttpMessageHandler : DelegatingHandler /// is . public SyncLoggingHttpMessageHandler(ILogger logger) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _logger = logger; } /// @@ -36,8 +36,8 @@ public SyncLoggingHttpMessageHandler(ILogger logger) /// or is . public SyncLoggingHttpMessageHandler(ILogger logger, HttpClientFactoryOptions options) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = logger; + _options = options; } protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken ct) diff --git a/src/BUTR.Site.NexusMods.Server/Utils/Http/Logging/SyncLoggingHttpMessageHandlerBuilderFilter.cs b/src/BUTR.Site.NexusMods.Server/Utils/Http/Logging/SyncLoggingHttpMessageHandlerBuilderFilter.cs index 0e5491f8..c9147e27 100644 --- a/src/BUTR.Site.NexusMods.Server/Utils/Http/Logging/SyncLoggingHttpMessageHandlerBuilderFilter.cs +++ b/src/BUTR.Site.NexusMods.Server/Utils/Http/Logging/SyncLoggingHttpMessageHandlerBuilderFilter.cs @@ -13,8 +13,8 @@ internal sealed class SyncLoggingHttpMessageHandlerBuilderFilter : IHttpMessageH public SyncLoggingHttpMessageHandlerBuilderFilter(ILoggerFactory loggerFactory, IOptionsMonitor optionsMonitor) { - _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - _optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor)); + _loggerFactory = loggerFactory; + _optionsMonitor = optionsMonitor; } public Action Configure(Action next) diff --git a/src/BUTR.Site.NexusMods.Server/Utils/Http/Logging/SyncLoggingScopeHttpMessageHandler.cs b/src/BUTR.Site.NexusMods.Server/Utils/Http/Logging/SyncLoggingScopeHttpMessageHandler.cs index cf26813e..7aa81b70 100644 --- a/src/BUTR.Site.NexusMods.Server/Utils/Http/Logging/SyncLoggingScopeHttpMessageHandler.cs +++ b/src/BUTR.Site.NexusMods.Server/Utils/Http/Logging/SyncLoggingScopeHttpMessageHandler.cs @@ -25,7 +25,7 @@ public class SyncLoggingScopeHttpMessageHandler : DelegatingHandler /// is . public SyncLoggingScopeHttpMessageHandler(ILogger logger) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _logger = logger; } /// @@ -36,8 +36,8 @@ public SyncLoggingScopeHttpMessageHandler(ILogger logger) /// or is . public SyncLoggingScopeHttpMessageHandler(ILogger logger, HttpClientFactoryOptions options) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = logger; + _options = options; } protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken ct) diff --git a/src/BUTR.Site.NexusMods.Server/Utils/Npgsql/PrepareCommandInterceptor.cs b/src/BUTR.Site.NexusMods.Server/Utils/Npgsql/PrepareCommandInterceptor.cs deleted file mode 100644 index a8d29ca5..00000000 --- a/src/BUTR.Site.NexusMods.Server/Utils/Npgsql/PrepareCommandInterceptor.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Microsoft.EntityFrameworkCore.Diagnostics; - -using System; -using System.Data; -using System.Data.Common; -using System.Threading; -using System.Threading.Tasks; - -namespace BUTR.Site.NexusMods.Server.Utils.Npgsql; - -public class PrepareCommandInterceptor : DbCommandInterceptor -{ - public const string Tag = "Prepare"; - - public override InterceptionResult ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) - { - Prepare(command); - return base.ReaderExecuting(command, eventData, result); - } - - public override ValueTask> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken ct = default) - { - Prepare(command); - return base.ReaderExecutingAsync(command, eventData, result, ct); - } - - public override ValueTask> NonQueryExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken ct = default) - { - Prepare(command); - return base.NonQueryExecutingAsync(command, eventData, result, ct); - } - - public override ValueTask> ScalarExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken ct = default) - { - Prepare(command); - return base.ScalarExecutingAsync(command, eventData, result, ct); - } - - public override InterceptionResult NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) - { - Prepare(command); - return base.NonQueryExecuting(command, eventData, result); - } - - public override InterceptionResult ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) - { - Prepare(command); - return base.ScalarExecuting(command, eventData, result); - } - - private static void Prepare(IDbCommand command) - { - if (!command.CommandText.Contains($"-- {Tag}", StringComparison.Ordinal)) - return; - - command.Prepare(); - } -} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Utils/NullableFilter.cs b/src/BUTR.Site.NexusMods.Server/Utils/NullableFilter.cs new file mode 100644 index 00000000..4a06bf12 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Utils/NullableFilter.cs @@ -0,0 +1,37 @@ +using Microsoft.OpenApi.Models; + +using Swashbuckle.AspNetCore.SwaggerGen; + +using System; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; + +namespace BUTR.Site.NexusMods.Server.Utils; + +public class NullableFilter : ISchemaFilter +{ + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + var nullabilityContext = new NullabilityInfoContext(); + var properties = context.Type.GetProperties(); + + foreach (var property in properties) + { + var jsonName = property.GetCustomAttribute()?.Name ?? property.Name; + var jsonKey = schema.Properties.Keys.SingleOrDefault(key => string.Equals(key, jsonName, StringComparison.OrdinalIgnoreCase)); + + if (string.IsNullOrWhiteSpace(jsonKey)) continue; + + schema.Properties[jsonKey].Nullable = nullabilityContext.Create(property).ReadState switch + { + NullabilityState.Unknown => false, + NullabilityState.NotNull => false, + NullabilityState.Nullable => true, + _ => false + }; + + schema.Required.Add(jsonKey); + } + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Utils/RequiredMemberFilter.cs b/src/BUTR.Site.NexusMods.Server/Utils/RequiredMemberFilter.cs index 7f29a660..b10e3d7b 100644 --- a/src/BUTR.Site.NexusMods.Server/Utils/RequiredMemberFilter.cs +++ b/src/BUTR.Site.NexusMods.Server/Utils/RequiredMemberFilter.cs @@ -30,8 +30,10 @@ public void Apply(OpenApiSchema schema, SchemaFilterContext context) // Ref types cannot be marked as nullable, so this would lead to them being non nullable. if (schema.Properties[jsonKey].Type is null && nullabilityContext.Create(property).ReadState == NullabilityState.Nullable) continue; + if (nullabilityContext.Create(property).ReadState == NullabilityState.Nullable) + schema.Properties[jsonKey].Nullable = true; + schema.Required.Add(jsonKey); } } - } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Utils/SlugifyActionConvention.cs b/src/BUTR.Site.NexusMods.Server/Utils/SlugifyActionConvention.cs new file mode 100644 index 00000000..a10a9200 --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Utils/SlugifyActionConvention.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Mvc.ApplicationModels; + +using System.Text.RegularExpressions; + +namespace BUTR.Site.NexusMods.Server.Utils; + +public partial class SlugifyActionConvention : IActionModelConvention +{ + [GeneratedRegex("([a-z])([A-Z])", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 100)] + private static partial Regex SlugifyRegex(); + + public void Apply(ActionModel action) + { + //action.ActionName = SlugifyRegex().Replace(action.ActionName, "$1-$2").ToLowerInvariant(); + foreach (var actionSelector in action.Selectors) + { + if (actionSelector.AttributeRouteModel?.Template is not null) + actionSelector.AttributeRouteModel.Template = SlugifyRegex().Replace(actionSelector.AttributeRouteModel.Template, "$1-$2").ToLowerInvariant(); + } + //action.Controller.ControllerName = SlugifyRegex().Replace(action.Controller.ControllerName, "$1-$2").ToLowerInvariant(); + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Utils/SlugifyParameterTransformer.cs b/src/BUTR.Site.NexusMods.Server/Utils/SlugifyParameterTransformer.cs new file mode 100644 index 00000000..f439c5fb --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Utils/SlugifyParameterTransformer.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Routing; + +using System.Text.RegularExpressions; + +namespace BUTR.Site.NexusMods.Server.Utils; + +public partial class SlugifyParameterTransformer : IOutboundParameterTransformer +{ + [GeneratedRegex("([a-z])([A-Z])", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 100)] + private static partial Regex SlugifyRegex(); + + public string? TransformOutbound(object? value) + { + if (value == null) return null; + + var str = value.ToString(); + if (string.IsNullOrEmpty(str)) return null; + + return SlugifyRegex().Replace(str, "$1-$2").ToLowerInvariant(); + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/Utils/SwaggerOperationIdFilter.cs b/src/BUTR.Site.NexusMods.Server/Utils/SwaggerOperationIdFilter.cs new file mode 100644 index 00000000..655eefde --- /dev/null +++ b/src/BUTR.Site.NexusMods.Server/Utils/SwaggerOperationIdFilter.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.OpenApi.Models; + +using Swashbuckle.AspNetCore.SwaggerGen; + +using System.Collections.Generic; +using System.Linq; + +namespace BUTR.Site.NexusMods.Server.Utils; + +public class SwaggerOperationIdFilter : IOperationFilter +{ + private readonly Dictionary _swaggerOperationIds = new Dictionary(); + + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + if (!(context.ApiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)) + { + return; + } + + if (_swaggerOperationIds.TryGetValue(controllerActionDescriptor.Id, out var id)) + { + operation.OperationId = id; + } + else + { + var operationIdBaseName = $"{controllerActionDescriptor.ControllerName}_{controllerActionDescriptor.ActionName}"; + var operationId = operationIdBaseName; + var suffix = 2; + while (_swaggerOperationIds.Values.Contains(operationId)) + { + operationId = $"{operationIdBaseName}{suffix++}"; + } + + _swaggerOperationIds[controllerActionDescriptor.Id] = operationId; + operation.OperationId = operationId; + } + } +} \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Server/appsettings.json b/src/BUTR.Site.NexusMods.Server/appsettings.json index d5fd411a..95d89965 100644 --- a/src/BUTR.Site.NexusMods.Server/appsettings.json +++ b/src/BUTR.Site.NexusMods.Server/appsettings.json @@ -14,7 +14,7 @@ } ] }, - + "Logging": { "LogLevel": { "Default": "Information", @@ -22,7 +22,7 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - + "CrashReporter": { "Endpoint": "https://report.butr.link/" }, diff --git a/src/BUTR.Site.NexusMods.ServerClient/BUTR.Site.NexusMods.ServerClient.csproj b/src/BUTR.Site.NexusMods.ServerClient/BUTR.Site.NexusMods.ServerClient.csproj index 6ca6c913..e8ea25c3 100644 --- a/src/BUTR.Site.NexusMods.ServerClient/BUTR.Site.NexusMods.ServerClient.csproj +++ b/src/BUTR.Site.NexusMods.ServerClient/BUTR.Site.NexusMods.ServerClient.csproj @@ -14,11 +14,14 @@ - + + + diff --git a/src/BUTR.Site.NexusMods.ServerClient/Clients.cs b/src/BUTR.Site.NexusMods.ServerClient/Clients.cs index 66f45b38..8e29502a 100644 --- a/src/BUTR.Site.NexusMods.ServerClient/Clients.cs +++ b/src/BUTR.Site.NexusMods.ServerClient/Clients.cs @@ -42,7 +42,7 @@ partial void PrepareRequest(HttpClient client, HttpRequestMessage request, Strin public virtual async Task> PaginatedStreamingAsync(PaginatedQuery body, CancellationToken ct = default) { var urlBuilder_ = new StringBuilder(); - urlBuilder_.Append("api/v1/CrashReports/PaginatedStreaming"); + urlBuilder_.Append("api/v1/crash-reports/paginated-streaming"); var client_ = _httpClient; var disposeClient_ = false; @@ -250,7 +250,7 @@ public ModsAnalyzerClient(HttpClient client, JsonSerializerOptions options) : th } } -public partial record NexusModsModModel +public partial record UserLinkedModModel { public string Url(string gameDomain) => $"https://nexusmods.com/{gameDomain}/mods/{NexusModsModId}"; } @@ -267,7 +267,7 @@ public partial record NexusModsArticleModel public string AuthorUrl() => $"https://nexusmods.com/users/{NexusModsUserId}"; } -public partial record ExposedNexusModsModModel +public partial record LinkedByExposureNexusModsModModelsModel { public string Url(string gameDomain) => $"https://nexusmods.com/{gameDomain}/mods/{NexusModsModId}"; } diff --git a/src/BUTR.Site.NexusMods.Shared/BUTR.Site.NexusMods.Shared.csproj b/src/BUTR.Site.NexusMods.Shared/BUTR.Site.NexusMods.Shared.csproj index b454fc28..95f94b9a 100644 --- a/src/BUTR.Site.NexusMods.Shared/BUTR.Site.NexusMods.Shared.csproj +++ b/src/BUTR.Site.NexusMods.Shared/BUTR.Site.NexusMods.Shared.csproj @@ -6,7 +6,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/src/BUTR.Site.NexusMods.Shared/Helpers/NexusModsUtils.cs b/src/BUTR.Site.NexusMods.Shared/Helpers/NexusModsUtils.cs index 81b64309..ac2433bb 100644 --- a/src/BUTR.Site.NexusMods.Shared/Helpers/NexusModsUtils.cs +++ b/src/BUTR.Site.NexusMods.Shared/Helpers/NexusModsUtils.cs @@ -5,7 +5,7 @@ namespace BUTR.Site.NexusMods.Shared.Helpers; public static class NexusModsUtils { - public static bool TryParse(string? url, [NotNullWhen(true)] out string? gameDomain, out uint modId) + public static bool TryParseModUrl(string? url, [NotNullWhen(true)] out string? gameDomain, out uint modId) { gameDomain = default; modId = default; @@ -31,4 +31,53 @@ public static bool TryParse(string? url, [NotNullWhen(true)] out string? gameDom modId = modIdNumber; return true; } + + public static bool TryParseUserId(string? url, [NotNullWhen(true)] out string? gameDomain, out uint userId) + { + gameDomain = default; + userId = default; + + if (url is null) + return false; + + if (!url.Contains("nexusmods.com/", StringComparison.OrdinalIgnoreCase)) + return false; + + var str1 = url.ToLowerInvariant().Split("nexusmods.com/"); + if (str1.Length != 2) + return false; + + var split = str1[1].Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length != 3) + return false; + + if (!uint.TryParse(split[2], out var userIdNumber)) + return false; + + gameDomain = split[0]; + userId = userIdNumber; + return true; + } + + public static bool TryParseUsername(string? url, [NotNullWhen(true)] out string? username) + { + username = default; + + if (url is null) + return false; + + if (!url.Contains("nexusmods.com/profile/", StringComparison.OrdinalIgnoreCase)) + return false; + + var str1 = url.ToLowerInvariant().Split("nexusmods.com/profile/"); + if (str1.Length != 2) + return false; + + var split = str1[1].Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length < 1) + return false; + + username = split[0]; + return true; + } } \ No newline at end of file diff --git a/src/BUTR.Site.NexusMods.Shared/Helpers/TenantUtils.cs b/src/BUTR.Site.NexusMods.Shared/Helpers/TenantUtils.cs index 33cdf34c..03c64901 100644 --- a/src/BUTR.Site.NexusMods.Shared/Helpers/TenantUtils.cs +++ b/src/BUTR.Site.NexusMods.Shared/Helpers/TenantUtils.cs @@ -17,6 +17,9 @@ public static class TenantUtils public const int StardewValleyId = 3; public const string StardewValleyName = "Stardew Valley"; public const string StardewValleyGameDomain = "stardewvalley"; + public const int ValheimId = 3; + public const string ValheimName = "Valheim"; + public const string ValheimGameDomain = "valheim"; private sealed record TenantMetadata(int Id, string NexusModsId, string Name); @@ -25,6 +28,7 @@ private sealed record TenantMetadata(int Id, string NexusModsId, string Name); new(BannerlordId, BannerlordGameDomain, BannerlordName), new(RimworldId, RimworldGameDomain, RimworldName), new(StardewValleyId, StardewValleyGameDomain, StardewValleyName), + new(ValheimId, ValheimGameDomain, ValheimName), }; public static int? FromGameDomainToTenant(string gameDomain) => TenantMetadatas.Find(x => string.Equals(x.NexusModsId, gameDomain, StringComparison.Ordinal))?.Id;