From ff92c5f3357173d56fe9662ca3e4f336ac08cea1 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 28 Oct 2024 14:04:55 +0100 Subject: [PATCH] excise entity framework from all remaining code --- .idea/.idea.catalogger/.idea/sqldialects.xml | 1 + .../Api/DiscordRequestService.cs | 33 +-- .../Api/GuildsController.Ignores.cs | 21 +- .../Api/GuildsController.Redirects.cs | 11 +- .../Api/GuildsController.Remove.cs | 2 - Catalogger.Backend/Api/GuildsController.cs | 4 +- Catalogger.Backend/Api/MetaController.cs | 2 - .../Middleware/AuthenticationMiddleware.cs | 10 +- .../Bot/Commands/ChannelCommands.cs | 4 +- .../Bot/Commands/ChannelCommandsComponents.cs | 2 +- .../Bot/Commands/IgnoreChannelCommands.cs | 2 +- .../Bot/Commands/InviteCommands.cs | 2 +- .../Bot/Commands/KeyRoleCommands.cs | 2 +- .../Bot/Commands/RedirectCommands.cs | 2 +- Catalogger.Backend/Bot/DiscordUtils.cs | 2 - .../Channels/ChannelCreateResponder.cs | 2 +- .../Channels/ChannelDeleteResponder.cs | 4 +- .../Channels/ChannelUpdateResponder.cs | 4 +- .../Responders/Guilds/AuditLogResponder.cs | 1 - .../Responders/Guilds/GuildBanAddResponder.cs | 4 +- .../Guilds/GuildBanRemoveResponder.cs | 4 +- .../Responders/Guilds/GuildCreateResponder.cs | 4 +- .../Guilds/GuildEmojisUpdateResponder.cs | 4 +- .../Responders/Guilds/GuildUpdateResponder.cs | 4 +- .../Invites/InviteCreateResponder.cs | 4 +- .../Invites/InviteDeleteResponder.cs | 2 +- .../Members/GuildMemberAddResponder.cs | 2 +- .../Members/GuildMemberRemoveResponder.cs | 2 +- .../Members/GuildMemberUpdateResponder.cs | 4 +- .../Messages/MessageCreateResponder.cs | 2 +- .../Messages/MessageDeleteBulkResponder.cs | 2 +- .../Messages/MessageDeleteResponder.cs | 2 +- .../Messages/MessageUpdateResponder.cs | 2 +- .../Responders/Roles/RoleCreateResponder.cs | 4 +- .../Responders/Roles/RoleDeleteResponder.cs | 2 +- .../Responders/Roles/RoleUpdateResponder.cs | 2 +- Catalogger.Backend/Catalogger.Backend.csproj | 17 +- .../{Dapper => }/DatabaseConnection.cs | 2 +- .../Database/DatabaseContext.cs | 124 --------- .../Database/DatabaseMigrator.cs | 145 +++++++++++ .../Database/{Dapper => }/DatabasePool.cs | 3 +- .../Database/Migrations/001_init.down.sql | 6 + .../Database/Migrations/001_init.up.sql | 60 +++++ .../20240803132306_Init.Designer.cs | 180 ------------- .../Migrations/20240803132306_Init.cs | 117 --------- ...41017130936_AddDashboardTokens.Designer.cs | 218 ---------------- .../20241017130936_AddDashboardTokens.cs | 47 ---- .../DatabaseContextModelSnapshot.cs | 215 ---------------- .../Database/Queries/QueryExtensions.cs | 56 ---- .../Repositories/ApiTokenRepository.cs | 97 +++++++ .../Repositories/GuildRepository.cs | 6 +- .../Repositories/InviteRepository.cs | 2 +- .../Repositories/MessageRepository.cs | 2 +- .../Repositories/WatchlistRepository.cs | 2 +- .../Database/setup_migrations.sql | 4 + .../Extensions/StartupExtensions.cs | 20 +- Catalogger.Backend/Program.cs | 4 +- .../Services/BackgroundTasksService.cs | 3 +- .../Services/GuildFetchService.cs | 1 - .../Services/MetricsCollectionService.cs | 3 - .../Services/WebhookExecutorService.cs | 4 +- Catalogger.Backend/packages.lock.json | 242 +----------------- 62 files changed, 402 insertions(+), 1339 deletions(-) rename Catalogger.Backend/Database/{Dapper => }/DatabaseConnection.cs (98%) delete mode 100644 Catalogger.Backend/Database/DatabaseContext.cs create mode 100644 Catalogger.Backend/Database/DatabaseMigrator.cs rename Catalogger.Backend/Database/{Dapper => }/DatabasePool.cs (98%) create mode 100644 Catalogger.Backend/Database/Migrations/001_init.down.sql create mode 100644 Catalogger.Backend/Database/Migrations/001_init.up.sql delete mode 100644 Catalogger.Backend/Database/Migrations/20240803132306_Init.Designer.cs delete mode 100644 Catalogger.Backend/Database/Migrations/20240803132306_Init.cs delete mode 100644 Catalogger.Backend/Database/Migrations/20241017130936_AddDashboardTokens.Designer.cs delete mode 100644 Catalogger.Backend/Database/Migrations/20241017130936_AddDashboardTokens.cs delete mode 100644 Catalogger.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs delete mode 100644 Catalogger.Backend/Database/Queries/QueryExtensions.cs create mode 100644 Catalogger.Backend/Database/Repositories/ApiTokenRepository.cs rename Catalogger.Backend/Database/{Dapper => }/Repositories/GuildRepository.cs (95%) rename Catalogger.Backend/Database/{Dapper => }/Repositories/InviteRepository.cs (97%) rename Catalogger.Backend/Database/{Dapper => }/Repositories/MessageRepository.cs (99%) rename Catalogger.Backend/Database/{Dapper => }/Repositories/WatchlistRepository.cs (96%) create mode 100644 Catalogger.Backend/Database/setup_migrations.sql diff --git a/.idea/.idea.catalogger/.idea/sqldialects.xml b/.idea/.idea.catalogger/.idea/sqldialects.xml index 6df4889..10eef95 100644 --- a/.idea/.idea.catalogger/.idea/sqldialects.xml +++ b/.idea/.idea.catalogger/.idea/sqldialects.xml @@ -1,6 +1,7 @@ + \ No newline at end of file diff --git a/Catalogger.Backend/Api/DiscordRequestService.cs b/Catalogger.Backend/Api/DiscordRequestService.cs index c61a850..abde930 100644 --- a/Catalogger.Backend/Api/DiscordRequestService.cs +++ b/Catalogger.Backend/Api/DiscordRequestService.cs @@ -15,8 +15,8 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; -using Catalogger.Backend.Database; using Catalogger.Backend.Database.Models; +using Catalogger.Backend.Database.Repositories; using NodaTime; namespace Catalogger.Backend.Api; @@ -28,7 +28,7 @@ public class DiscordRequestService private readonly ApiCache _apiCache; private readonly Config _config; private readonly IClock _clock; - private readonly DatabaseContext _db; + private readonly ApiTokenRepository _tokenRepository; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }; @@ -38,14 +38,14 @@ public class DiscordRequestService ApiCache apiCache, Config config, IClock clock, - DatabaseContext db + ApiTokenRepository tokenRepository ) { _logger = logger.ForContext(); _apiCache = apiCache; _config = config; _clock = clock; - _db = db; + _tokenRepository = tokenRepository; _httpClient = new HttpClient(); _httpClient.DefaultRequestHeaders.Add( @@ -154,16 +154,13 @@ public class DiscordRequestService var meUser = await GetMeAsync($"Bearer {token.AccessToken}"); var meGuilds = await GetGuildsAsync($"Bearer {token.AccessToken}"); - var apiToken = new ApiToken - { - DashboardToken = ApiUtils.RandomToken(64), - UserId = meUser.Id, - AccessToken = token.AccessToken, - RefreshToken = token.RefreshToken, - ExpiresAt = _clock.GetCurrentInstant() + Duration.FromSeconds(token.ExpiresIn), - }; - _db.Add(apiToken); - await _db.SaveChangesAsync(ct); + var apiToken = await _tokenRepository.CreateAsync( + ApiUtils.RandomToken(64), + meUser.Id, + token.AccessToken, + token.RefreshToken, + token.ExpiresIn + ); return (apiToken, meUser, meGuilds); } @@ -231,8 +228,12 @@ public class DiscordRequestService token.RefreshToken = discordToken.RefreshToken; token.ExpiresAt = _clock.GetCurrentInstant() + Duration.FromSeconds(discordToken.ExpiresIn); - _db.Update(token); - await _db.SaveChangesAsync(); + await _tokenRepository.UpdateAsync( + token.Id, + discordToken.AccessToken, + discordToken.RefreshToken, + discordToken.ExpiresIn + ); } [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] diff --git a/Catalogger.Backend/Api/GuildsController.Ignores.cs b/Catalogger.Backend/Api/GuildsController.Ignores.cs index d2b1ec4..ea9e647 100644 --- a/Catalogger.Backend/Api/GuildsController.Ignores.cs +++ b/Catalogger.Backend/Api/GuildsController.Ignores.cs @@ -13,7 +13,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using Catalogger.Backend.Database.Queries; using Microsoft.AspNetCore.Mvc; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Objects; @@ -26,7 +25,7 @@ public partial class GuildsController public async Task AddIgnoredChannelAsync(string id, ulong channelId) { var (guildId, _) = await ParseGuildAsync(id); - var guildConfig = await db.GetGuildAsync(guildId); + var guildConfig = await guildRepository.GetAsync(guildId); if (guildConfig.Channels.IgnoredChannels.Contains(channelId)) return NoContent(); @@ -47,8 +46,7 @@ public partial class GuildsController return NoContent(); guildConfig.Channels.IgnoredChannels.Add(channelId); - db.Update(guildConfig); - await db.SaveChangesAsync(); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); return NoContent(); } @@ -57,11 +55,10 @@ public partial class GuildsController public async Task RemoveIgnoredChannelAsync(string id, ulong channelId) { var (guildId, _) = await ParseGuildAsync(id); - var guildConfig = await db.GetGuildAsync(guildId); + var guildConfig = await guildRepository.GetAsync(guildId); guildConfig.Channels.IgnoredChannels.Remove(channelId); - db.Update(guildConfig); - await db.SaveChangesAsync(); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); return NoContent(); } @@ -81,7 +78,7 @@ public partial class GuildsController public async Task AddIgnoredUserAsync(string id, ulong userId) { var (guildId, _) = await ParseGuildAsync(id); - var guildConfig = await db.GetGuildAsync(guildId); + var guildConfig = await guildRepository.GetAsync(guildId); if (guildConfig.Channels.IgnoredUsers.Contains(userId)) return NoContent(); @@ -91,8 +88,7 @@ public partial class GuildsController return NoContent(); guildConfig.Channels.IgnoredUsers.Add(userId); - db.Update(guildConfig); - await db.SaveChangesAsync(); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); return NoContent(); } @@ -101,11 +97,10 @@ public partial class GuildsController public async Task RemoveIgnoredUserAsync(string id, ulong userId) { var (guildId, _) = await ParseGuildAsync(id); - var guildConfig = await db.GetGuildAsync(guildId); + var guildConfig = await guildRepository.GetAsync(guildId); guildConfig.Channels.IgnoredUsers.Remove(userId); - db.Update(guildConfig); - await db.SaveChangesAsync(); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); return NoContent(); } diff --git a/Catalogger.Backend/Api/GuildsController.Redirects.cs b/Catalogger.Backend/Api/GuildsController.Redirects.cs index 9c9f11a..9ce84de 100644 --- a/Catalogger.Backend/Api/GuildsController.Redirects.cs +++ b/Catalogger.Backend/Api/GuildsController.Redirects.cs @@ -15,7 +15,6 @@ using System.Net; using Catalogger.Backend.Api.Middleware; -using Catalogger.Backend.Database.Queries; using Microsoft.AspNetCore.Mvc; using Remora.Discord.API.Abstractions.Objects; @@ -31,7 +30,7 @@ public partial class GuildsController { var (guildId, _) = await ParseGuildAsync(id); var guildChannels = channelCache.GuildChannels(guildId).ToList(); - var guildConfig = await db.GetGuildAsync(guildId.Value); + var guildConfig = await guildRepository.GetAsync(guildId); Console.WriteLine($"Source: {req.Source}, target: {req.Target}"); @@ -62,8 +61,7 @@ public partial class GuildsController ); guildConfig.Channels.Redirects[source.ID.Value] = target.ID.Value; - db.Update(guildConfig); - await db.SaveChangesAsync(); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); return NoContent(); } @@ -72,7 +70,7 @@ public partial class GuildsController public async Task DeleteRedirectAsync(string id, ulong channelId) { var (guildId, _) = await ParseGuildAsync(id); - var guildConfig = await db.GetGuildAsync(guildId.Value); + var guildConfig = await guildRepository.GetAsync(guildId); if (!guildConfig.Channels.Redirects.ContainsKey(channelId)) throw new ApiError( @@ -82,8 +80,7 @@ public partial class GuildsController ); guildConfig.Channels.Redirects.Remove(channelId, out _); - db.Update(guildConfig); - await db.SaveChangesAsync(); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); return NoContent(); } diff --git a/Catalogger.Backend/Api/GuildsController.Remove.cs b/Catalogger.Backend/Api/GuildsController.Remove.cs index 08f0d61..2d2ff5e 100644 --- a/Catalogger.Backend/Api/GuildsController.Remove.cs +++ b/Catalogger.Backend/Api/GuildsController.Remove.cs @@ -16,12 +16,10 @@ using System.Net; using Catalogger.Backend.Api.Middleware; using Catalogger.Backend.Bot; -using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Dapper; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Remora.Discord.Extensions.Embeds; namespace Catalogger.Backend.Api; diff --git a/Catalogger.Backend/Api/GuildsController.cs b/Catalogger.Backend/Api/GuildsController.cs index 6517ae2..560456f 100644 --- a/Catalogger.Backend/Api/GuildsController.cs +++ b/Catalogger.Backend/Api/GuildsController.cs @@ -18,8 +18,7 @@ using Catalogger.Backend.Api.Middleware; using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Dapper; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Services; using Microsoft.AspNetCore.Mvc; using Remora.Discord.API; @@ -32,7 +31,6 @@ namespace Catalogger.Backend.Api; [Route("/api/guilds/{id}")] public partial class GuildsController( ILogger logger, - DatabaseContext db, DatabaseConnection dbConn, GuildRepository guildRepository, GuildCache guildCache, diff --git a/Catalogger.Backend/Api/MetaController.cs b/Catalogger.Backend/Api/MetaController.cs index 533bc6d..17d3c27 100644 --- a/Catalogger.Backend/Api/MetaController.cs +++ b/Catalogger.Backend/Api/MetaController.cs @@ -15,10 +15,8 @@ using Catalogger.Backend.Api.Middleware; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Microsoft.AspNetCore.Mvc; -using Remora.Discord.API.Abstractions.Objects; namespace Catalogger.Backend.Api; diff --git a/Catalogger.Backend/Api/Middleware/AuthenticationMiddleware.cs b/Catalogger.Backend/Api/Middleware/AuthenticationMiddleware.cs index 21d339a..47447dc 100644 --- a/Catalogger.Backend/Api/Middleware/AuthenticationMiddleware.cs +++ b/Catalogger.Backend/Api/Middleware/AuthenticationMiddleware.cs @@ -14,14 +14,14 @@ // along with this program. If not, see . using System.Net; -using Catalogger.Backend.Database; using Catalogger.Backend.Database.Models; -using Microsoft.EntityFrameworkCore; +using Catalogger.Backend.Database.Repositories; using NodaTime; namespace Catalogger.Backend.Api.Middleware; -public class AuthenticationMiddleware(DatabaseContext db, IClock clock) : IMiddleware +public class AuthenticationMiddleware(ApiTokenRepository tokenRepository, IClock clock) + : IMiddleware { public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { @@ -37,9 +37,7 @@ public class AuthenticationMiddleware(DatabaseContext db, IClock clock) : IMiddl var token = ctx.Request.Headers.Authorization.ToString(); - var apiToken = await db.ApiTokens.FirstOrDefaultAsync(t => - t.DashboardToken == token && t.ExpiresAt > clock.GetCurrentInstant() - ); + var apiToken = await tokenRepository.GetAsync(token); if (apiToken == null) { if (requireAuth) diff --git a/Catalogger.Backend/Bot/Commands/ChannelCommands.cs b/Catalogger.Backend/Bot/Commands/ChannelCommands.cs index 438459a..fcf54b9 100644 --- a/Catalogger.Backend/Bot/Commands/ChannelCommands.cs +++ b/Catalogger.Backend/Bot/Commands/ChannelCommands.cs @@ -16,9 +16,7 @@ using System.ComponentModel; using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Dapper.Repositories; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Commands.Attributes; diff --git a/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs b/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs index 38bfc71..1bda457 100644 --- a/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs +++ b/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs @@ -14,7 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Objects; diff --git a/Catalogger.Backend/Bot/Commands/IgnoreChannelCommands.cs b/Catalogger.Backend/Bot/Commands/IgnoreChannelCommands.cs index afa9165..63dce5e 100644 --- a/Catalogger.Backend/Bot/Commands/IgnoreChannelCommands.cs +++ b/Catalogger.Backend/Bot/Commands/IgnoreChannelCommands.cs @@ -16,7 +16,7 @@ using System.ComponentModel; using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Commands.Attributes; diff --git a/Catalogger.Backend/Bot/Commands/InviteCommands.cs b/Catalogger.Backend/Bot/Commands/InviteCommands.cs index 68d11f9..77757f6 100644 --- a/Catalogger.Backend/Bot/Commands/InviteCommands.cs +++ b/Catalogger.Backend/Bot/Commands/InviteCommands.cs @@ -17,7 +17,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs b/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs index 94a1bcd..67d3ab8 100644 --- a/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs +++ b/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs @@ -15,7 +15,7 @@ using System.ComponentModel; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/Catalogger.Backend/Bot/Commands/RedirectCommands.cs b/Catalogger.Backend/Bot/Commands/RedirectCommands.cs index 86e272f..c776675 100644 --- a/Catalogger.Backend/Bot/Commands/RedirectCommands.cs +++ b/Catalogger.Backend/Bot/Commands/RedirectCommands.cs @@ -15,7 +15,7 @@ using System.ComponentModel; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/Catalogger.Backend/Bot/DiscordUtils.cs b/Catalogger.Backend/Bot/DiscordUtils.cs index 87bca46..8e0a867 100644 --- a/Catalogger.Backend/Bot/DiscordUtils.cs +++ b/Catalogger.Backend/Bot/DiscordUtils.cs @@ -17,9 +17,7 @@ using System.Drawing; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Objects; -using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Pagination; -using Remora.Discord.Pagination.Extensions; using Remora.Rest.Core; namespace Catalogger.Backend.Bot; diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs index b2d5f50..93666fe 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs @@ -14,7 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs index 78b5e89..aaee939 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs @@ -14,9 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Dapper.Repositories; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs index ea762f9..0ad0c08 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs @@ -14,9 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Dapper.Repositories; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Humanizer; diff --git a/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs index 1d9eae2..1d88db1 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs @@ -14,7 +14,6 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Remora.Discord.API; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.Gateway.Responders; diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildBanAddResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildBanAddResponder.cs index 2001efb..0100b1f 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildBanAddResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildBanAddResponder.cs @@ -14,9 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Dapper.Repositories; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildBanRemoveResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildBanRemoveResponder.cs index 0885298..ee1d1bc 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildBanRemoveResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildBanRemoveResponder.cs @@ -14,9 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Dapper.Repositories; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs index 0ee92df..ab12125 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs @@ -15,15 +15,13 @@ using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Results; -using Guild = Catalogger.Backend.Database.Models.Guild; namespace Catalogger.Backend.Bot.Responders.Guilds; diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs index ebdd9bb..0ccc859 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs @@ -14,9 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Dapper.Repositories; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API; diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildUpdateResponder.cs index 89ef472..ee43e98 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildUpdateResponder.cs @@ -14,9 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Dapper.Repositories; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API; diff --git a/Catalogger.Backend/Bot/Responders/Invites/InviteCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Invites/InviteCreateResponder.cs index 225f04a..329d764 100644 --- a/Catalogger.Backend/Bot/Responders/Invites/InviteCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Invites/InviteCreateResponder.cs @@ -14,9 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Dapper.Repositories; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; diff --git a/Catalogger.Backend/Bot/Responders/Invites/InviteDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Invites/InviteDeleteResponder.cs index b33ad94..bb9d855 100644 --- a/Catalogger.Backend/Bot/Responders/Invites/InviteDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Invites/InviteDeleteResponder.cs @@ -14,7 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; diff --git a/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs b/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs index dfb56f5..5ea725a 100644 --- a/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs @@ -15,7 +15,7 @@ using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Humanizer; diff --git a/Catalogger.Backend/Bot/Responders/Members/GuildMemberRemoveResponder.cs b/Catalogger.Backend/Bot/Responders/Members/GuildMemberRemoveResponder.cs index bbeea22..94cf8cc 100644 --- a/Catalogger.Backend/Bot/Responders/Members/GuildMemberRemoveResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Members/GuildMemberRemoveResponder.cs @@ -15,7 +15,7 @@ using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; diff --git a/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs index a4dac29..4a6e6b8 100644 --- a/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs @@ -15,9 +15,7 @@ using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Dapper.Repositories; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs index 8364dcf..cc45edc 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs @@ -15,7 +15,7 @@ using System.Text.RegularExpressions; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Humanizer; diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteBulkResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteBulkResponder.cs index b9c66a4..6676839 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteBulkResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteBulkResponder.cs @@ -15,7 +15,7 @@ using System.Text; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using NodaTime.Extensions; diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs index d97b09d..f866b94 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs @@ -14,7 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Humanizer; diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs index d042765..1dd9290 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs @@ -14,7 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API; diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs index 6dc6374..4df76ea 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs @@ -14,9 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Dapper.Repositories; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs index 303f7dd..8566434 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs @@ -14,7 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs index c6d142b..828ef22 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs @@ -14,7 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Dapper.Repositories; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Humanizer; diff --git a/Catalogger.Backend/Catalogger.Backend.csproj b/Catalogger.Backend/Catalogger.Backend.csproj index be55129..436899d 100644 --- a/Catalogger.Backend/Catalogger.Backend.csproj +++ b/Catalogger.Backend/Catalogger.Backend.csproj @@ -7,25 +7,20 @@ true + + + + - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - @@ -42,4 +37,8 @@ + + + + diff --git a/Catalogger.Backend/Database/Dapper/DatabaseConnection.cs b/Catalogger.Backend/Database/DatabaseConnection.cs similarity index 98% rename from Catalogger.Backend/Database/Dapper/DatabaseConnection.cs rename to Catalogger.Backend/Database/DatabaseConnection.cs index ed6bafe..61d2a7e 100644 --- a/Catalogger.Backend/Database/Dapper/DatabaseConnection.cs +++ b/Catalogger.Backend/Database/DatabaseConnection.cs @@ -18,7 +18,7 @@ using System.Data.Common; using System.Diagnostics.CodeAnalysis; using Npgsql; -namespace Catalogger.Backend.Database.Dapper; +namespace Catalogger.Backend.Database; public class DatabaseConnection(Guid id, ILogger logger, NpgsqlConnection inner) : DbConnection, diff --git a/Catalogger.Backend/Database/DatabaseContext.cs b/Catalogger.Backend/Database/DatabaseContext.cs deleted file mode 100644 index 0fa4fb4..0000000 --- a/Catalogger.Backend/Database/DatabaseContext.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (C) 2021-present sam (starshines.gay) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -using Catalogger.Backend.Database.Models; -using Catalogger.Backend.Extensions; -using EntityFramework.Exceptions.PostgreSQL; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.EntityFrameworkCore.Design; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql; - -namespace Catalogger.Backend.Database; - -public class DatabaseContext : DbContext -{ - private readonly NpgsqlDataSource _dataSource; - private readonly ILoggerFactory? _loggerFactory; - - public DbSet Guilds { get; set; } - public DbSet Messages { get; set; } - public DbSet IgnoredMessages { get; set; } - public DbSet Invites { get; set; } - public DbSet Watchlists { get; set; } - public DbSet ApiTokens { get; set; } - - public DatabaseContext(Config config, ILoggerFactory? loggerFactory) - { - var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) - { - Timeout = config.Database.Timeout ?? 5, - MaxPoolSize = config.Database.MaxPoolSize ?? 50, - }.ConnectionString; - - var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); - dataSourceBuilder.EnableDynamicJson().UseNodaTime(); - _dataSource = dataSourceBuilder.Build(); - _loggerFactory = loggerFactory; - } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => - optionsBuilder - .ConfigureWarnings(c => - c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning) - .Ignore(CoreEventId.SaveChangesFailed) - ) - .UseNpgsql(_dataSource, o => o.UseNodaTime()) - .UseSnakeCaseNamingConvention() - .UseLoggerFactory(_loggerFactory) - .UseExceptionProcessor(); - - protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) - { - configurationBuilder.Properties().HaveConversion(); - configurationBuilder.Properties>().HaveConversion(); - } - - private static readonly ValueComparer> UlongListValueComparer = - new( - (c1, c2) => c1 != null && c2 != null && c1.SequenceEqual(c2), - c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())) - ); - - private static readonly ValueComparer UlongArrayValueComparer = - new( - (c1, c2) => c1 != null && c2 != null && c1.SequenceEqual(c2), - c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())) - ); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .Property(g => g.KeyRoles) - .Metadata.SetValueComparer(UlongArrayValueComparer); - - modelBuilder.Entity().HasKey(i => i.Code); - modelBuilder.Entity().HasIndex(i => i.GuildId); - - modelBuilder.Entity().HasKey(w => new { w.GuildId, w.UserId }); - modelBuilder.Entity().Property(w => w.AddedAt).HasDefaultValueSql("now()"); - } -} - -public class DesignTimeDatabaseContextFactory : IDesignTimeDbContextFactory -{ - public DatabaseContext CreateDbContext(string[] args) - { - // Read the configuration file - var config = - new ConfigurationBuilder() - .AddConfiguration() - .Build() - // Get the configuration as our config class - .Get() ?? new(); - - return new DatabaseContext(config, null); - } -} - -public class UlongValueConverter() - : ValueConverter( - convertToProviderExpression: x => (long)x, - convertFromProviderExpression: x => (ulong)x - ); - -public class UlongArrayValueConverter() - : ValueConverter, List>( - convertToProviderExpression: x => x.Select(i => (long)i).ToList(), - convertFromProviderExpression: x => x.Select(i => (ulong)i).ToList() - ); diff --git a/Catalogger.Backend/Database/DatabaseMigrator.cs b/Catalogger.Backend/Database/DatabaseMigrator.cs new file mode 100644 index 0000000..76a0dbc --- /dev/null +++ b/Catalogger.Backend/Database/DatabaseMigrator.cs @@ -0,0 +1,145 @@ +// Copyright (C) 2021-present sam (starshines.gay) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.Data.Common; +using Dapper; +using NodaTime; + +namespace Catalogger.Backend.Database; + +public class DatabaseMigrator(ILogger logger, IClock clock, DatabaseConnection conn) + : IDisposable, + IAsyncDisposable +{ + private const string RootPath = "Catalogger.Backend.Database"; + private static readonly int MigrationsPathLength = $"{RootPath}.Migrations.".Length; + + public async Task Migrate() + { + var migrations = GetMigrationNames().ToArray(); + logger.Debug("Getting current database migration"); + var currentMigration = await GetCurrentMigration(); + if (currentMigration != null) + migrations = migrations + .Where(s => string.CompareOrdinal(s, currentMigration.MigrationName) > 0) + .ToArray(); + + logger.Information( + "Current migration: {Migration}. Applying {Count} migrations", + currentMigration?.MigrationName, + migrations.Length + ); + if (migrations.Length == 0) + { + return; + } + + // Wrap all migrations in a transaction + await using var tx = await conn.BeginTransactionAsync(); + var totalStartTime = clock.GetCurrentInstant(); + foreach (var migration in migrations) + { + logger.Debug("Executing migration {Migration}", migration); + var startTime = clock.GetCurrentInstant(); + await ExecuteMigration(tx, migration); + var took = clock.GetCurrentInstant() - startTime; + logger.Debug("Executed migration {Migration} in {Took}", migration, took); + } + + var totalTook = clock.GetCurrentInstant() - totalStartTime; + logger.Information("Executed {Count} migrations in {Took}", migrations.Length, totalTook); + + // Finally, commit the transaction + await tx.CommitAsync(); + } + + private async Task ExecuteMigration(DbTransaction tx, string migrationName) + { + var query = await GetResource($"{RootPath}.Migrations.{migrationName}.up.sql"); + + // Run the migration + await conn.ExecuteAsync(query, transaction: tx); + // Store that we ran the migration + await conn.ExecuteAsync( + "INSERT INTO migrations (migration_name, applied_at) VALUES (@MigrationName, @AppliedAt)", + new { MigrationName = migrationName, AppliedAt = clock.GetCurrentInstant() } + ); + } + + /// Returns the current migration. If no migrations have been applied, returns null + private async Task GetCurrentMigration() + { + // Check if the migrations table exists + var hasMigrationTable = + await conn.QuerySingleOrDefaultAsync( + "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'migrations'" + ) == 1; + // If so, return the current migration + if (hasMigrationTable) + { + return await conn.QuerySingleOrDefaultAsync( + "SELECT * FROM migrations ORDER BY applied_at DESC LIMIT 1" + ); + } + + logger.Debug("Migrations table does not exist, assuming this is a new database"); + + // Else, create the migrations table then return null + var migrationTableQuery = await GetResource($"{RootPath}.setup_migrations.sql"); + await conn.ExecuteAsync(migrationTableQuery); + return null; + } + + /// Returns a resource by name as a string. + private static async Task GetResource(string name) + { + await using var stream = + typeof(DatabasePool).Assembly.GetManifestResourceStream(name) + ?? throw new ArgumentException($"Invalid resource '{name}'"); + using var reader = new StreamReader(stream); + return await reader.ReadToEndAsync(); + } + + public static IEnumerable GetMigrationNames() => + typeof(DatabasePool) + .Assembly.GetManifestResourceNames() + .Where(s => s.StartsWith($"{RootPath}.Migrations")) + .Where(s => s.EndsWith(".up.sql")) + .Select(s => + s.Substring( + MigrationsPathLength, + s.Length - MigrationsPathLength - ".up.sql".Length + ) + ) + .OrderBy(s => s); + + private record MigrationEntry + { + public string MigrationName { get; init; } = null!; + public Instant AppliedAt { get; init; } + } + + public void Dispose() + { + conn.Dispose(); + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + await conn.DisposeAsync(); + GC.SuppressFinalize(this); + } +} diff --git a/Catalogger.Backend/Database/Dapper/DatabasePool.cs b/Catalogger.Backend/Database/DatabasePool.cs similarity index 98% rename from Catalogger.Backend/Database/Dapper/DatabasePool.cs rename to Catalogger.Backend/Database/DatabasePool.cs index 3d1342f..82072f8 100644 --- a/Catalogger.Backend/Database/Dapper/DatabasePool.cs +++ b/Catalogger.Backend/Database/DatabasePool.cs @@ -15,13 +15,12 @@ using System.Data; using System.Text.Json; -using System.Text.Json.Serialization; using Catalogger.Backend.Database.Models; using Dapper; using NodaTime; using Npgsql; -namespace Catalogger.Backend.Database.Dapper; +namespace Catalogger.Backend.Database; public class DatabasePool { diff --git a/Catalogger.Backend/Database/Migrations/001_init.down.sql b/Catalogger.Backend/Database/Migrations/001_init.down.sql new file mode 100644 index 0000000..a8b2cb7 --- /dev/null +++ b/Catalogger.Backend/Database/Migrations/001_init.down.sql @@ -0,0 +1,6 @@ +drop table api_tokens; +drop table watchlists; +drop table messages; +drop table invites; +drop table ignored_messages; +drop table guilds; diff --git a/Catalogger.Backend/Database/Migrations/001_init.up.sql b/Catalogger.Backend/Database/Migrations/001_init.up.sql new file mode 100644 index 0000000..43fc84d --- /dev/null +++ b/Catalogger.Backend/Database/Migrations/001_init.up.sql @@ -0,0 +1,60 @@ +create table if not exists guilds +( + id bigint not null primary key, + channels jsonb not null, + banned_systems text[] not null, + key_roles bigint[] not null +); + +create table if not exists ignored_messages +( + id bigint not null primary key +); + +create table if not exists invites +( + code text not null primary key, + guild_id bigint not null, + name text not null +); + +create index if not exists ix_invites_guild_id on invites (guild_id); + +create table if not exists messages +( + id bigint not null primary key, + original_id bigint, + user_id bigint not null, + channel_id bigint not null, + guild_id bigint not null, + member text, + system text, + username bytea not null, + content bytea not null, + metadata bytea, + attachment_size integer not null +); + +create table if not exists watchlists +( + guild_id bigint not null, + user_id bigint not null, + added_at timestamp with time zone default now() not null, + moderator_id bigint not null, + reason text not null, + + primary key (guild_id, user_id) +); + +create table if not exists api_tokens +( + id integer generated by default as identity primary key, + dashboard_token text not null, + user_id text not null, + access_token text not null, + refresh_token text, + expires_at timestamp with time zone not null +); + +-- Finally, drop the EFCore migrations table. +drop table if exists "__EFMigrationsHistory"; diff --git a/Catalogger.Backend/Database/Migrations/20240803132306_Init.Designer.cs b/Catalogger.Backend/Database/Migrations/20240803132306_Init.Designer.cs deleted file mode 100644 index 9167588..0000000 --- a/Catalogger.Backend/Database/Migrations/20240803132306_Init.Designer.cs +++ /dev/null @@ -1,180 +0,0 @@ -// -using System.Collections.Generic; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Catalogger.Backend.Database.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20240803132306_Init")] - partial class Init - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.Guild", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property>("BannedSystems") - .IsRequired() - .HasColumnType("text[]") - .HasColumnName("banned_systems"); - - b.Property("Channels") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("channels"); - - b.Property>("KeyRoles") - .IsRequired() - .HasColumnType("bigint[]") - .HasColumnName("key_roles"); - - b.HasKey("Id") - .HasName("pk_guilds"); - - b.ToTable("guilds", (string)null); - }); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.IgnoredMessage", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.HasKey("Id") - .HasName("pk_ignored_messages"); - - b.ToTable("ignored_messages", (string)null); - }); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.Invite", b => - { - b.Property("Code") - .HasColumnType("text") - .HasColumnName("code"); - - b.Property("GuildId") - .HasColumnType("bigint") - .HasColumnName("guild_id"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.HasKey("Code") - .HasName("pk_invites"); - - b.HasIndex("GuildId") - .HasDatabaseName("ix_invites_guild_id"); - - b.ToTable("invites", (string)null); - }); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.Message", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("AttachmentSize") - .HasColumnType("integer") - .HasColumnName("attachment_size"); - - b.Property("ChannelId") - .HasColumnType("bigint") - .HasColumnName("channel_id"); - - b.Property("EncryptedContent") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("content"); - - b.Property("EncryptedMetadata") - .HasColumnType("bytea") - .HasColumnName("metadata"); - - b.Property("EncryptedUsername") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("username"); - - b.Property("GuildId") - .HasColumnType("bigint") - .HasColumnName("guild_id"); - - b.Property("Member") - .HasColumnType("text") - .HasColumnName("member"); - - b.Property("OriginalId") - .HasColumnType("bigint") - .HasColumnName("original_id"); - - b.Property("System") - .HasColumnType("text") - .HasColumnName("system"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_messages"); - - b.ToTable("messages", (string)null); - }); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.Watchlist", b => - { - b.Property("GuildId") - .HasColumnType("bigint") - .HasColumnName("guild_id"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.Property("AddedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("added_at") - .HasDefaultValueSql("now()"); - - b.Property("ModeratorId") - .HasColumnType("bigint") - .HasColumnName("moderator_id"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text") - .HasColumnName("reason"); - - b.HasKey("GuildId", "UserId") - .HasName("pk_watchlists"); - - b.ToTable("watchlists", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Catalogger.Backend/Database/Migrations/20240803132306_Init.cs b/Catalogger.Backend/Database/Migrations/20240803132306_Init.cs deleted file mode 100644 index 51c7218..0000000 --- a/Catalogger.Backend/Database/Migrations/20240803132306_Init.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Collections.Generic; -using Catalogger.Backend.Database.Models; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; - -#nullable disable - -namespace Catalogger.Backend.Database.Migrations -{ - /// - public partial class Init : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "guilds", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false), - channels = table.Column(type: "jsonb", nullable: false), - banned_systems = table.Column>(type: "text[]", nullable: false), - key_roles = table.Column>(type: "bigint[]", nullable: false), - }, - constraints: table => - { - table.PrimaryKey("pk_guilds", x => x.id); - } - ); - - migrationBuilder.CreateTable( - name: "ignored_messages", - columns: table => new { id = table.Column(type: "bigint", nullable: false) }, - constraints: table => - { - table.PrimaryKey("pk_ignored_messages", x => x.id); - } - ); - - migrationBuilder.CreateTable( - name: "invites", - columns: table => new - { - code = table.Column(type: "text", nullable: false), - guild_id = table.Column(type: "bigint", nullable: false), - name = table.Column(type: "text", nullable: false), - }, - constraints: table => - { - table.PrimaryKey("pk_invites", x => x.code); - } - ); - - migrationBuilder.CreateTable( - name: "messages", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false), - original_id = table.Column(type: "bigint", nullable: true), - user_id = table.Column(type: "bigint", nullable: false), - channel_id = table.Column(type: "bigint", nullable: false), - guild_id = table.Column(type: "bigint", nullable: false), - member = table.Column(type: "text", nullable: true), - system = table.Column(type: "text", nullable: true), - username = table.Column(type: "bytea", nullable: false), - content = table.Column(type: "bytea", nullable: false), - metadata = table.Column(type: "bytea", nullable: true), - attachment_size = table.Column(type: "integer", nullable: false), - }, - constraints: table => - { - table.PrimaryKey("pk_messages", x => x.id); - } - ); - - migrationBuilder.CreateTable( - name: "watchlists", - columns: table => new - { - guild_id = table.Column(type: "bigint", nullable: false), - user_id = table.Column(type: "bigint", nullable: false), - added_at = table.Column( - type: "timestamp with time zone", - nullable: false, - defaultValueSql: "now()" - ), - moderator_id = table.Column(type: "bigint", nullable: false), - reason = table.Column(type: "text", nullable: false), - }, - constraints: table => - { - table.PrimaryKey("pk_watchlists", x => new { x.guild_id, x.user_id }); - } - ); - - migrationBuilder.CreateIndex( - name: "ix_invites_guild_id", - table: "invites", - column: "guild_id" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "guilds"); - - migrationBuilder.DropTable(name: "ignored_messages"); - - migrationBuilder.DropTable(name: "invites"); - - migrationBuilder.DropTable(name: "messages"); - - migrationBuilder.DropTable(name: "watchlists"); - } - } -} diff --git a/Catalogger.Backend/Database/Migrations/20241017130936_AddDashboardTokens.Designer.cs b/Catalogger.Backend/Database/Migrations/20241017130936_AddDashboardTokens.Designer.cs deleted file mode 100644 index 2399bb8..0000000 --- a/Catalogger.Backend/Database/Migrations/20241017130936_AddDashboardTokens.Designer.cs +++ /dev/null @@ -1,218 +0,0 @@ -// -using System.Collections.Generic; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Catalogger.Backend.Database.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20241017130936_AddDashboardTokens")] - partial class AddDashboardTokens - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.ApiToken", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("text") - .HasColumnName("access_token"); - - b.Property("DashboardToken") - .IsRequired() - .HasColumnType("text") - .HasColumnName("dashboard_token"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires_at"); - - b.Property("RefreshToken") - .HasColumnType("text") - .HasColumnName("refresh_token"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("text") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_api_tokens"); - - b.ToTable("api_tokens", (string)null); - }); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.Guild", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property>("BannedSystems") - .IsRequired() - .HasColumnType("text[]") - .HasColumnName("banned_systems"); - - b.Property("Channels") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("channels"); - - b.Property>("KeyRoles") - .IsRequired() - .HasColumnType("bigint[]") - .HasColumnName("key_roles"); - - b.HasKey("Id") - .HasName("pk_guilds"); - - b.ToTable("guilds", (string)null); - }); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.IgnoredMessage", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.HasKey("Id") - .HasName("pk_ignored_messages"); - - b.ToTable("ignored_messages", (string)null); - }); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.Invite", b => - { - b.Property("Code") - .HasColumnType("text") - .HasColumnName("code"); - - b.Property("GuildId") - .HasColumnType("bigint") - .HasColumnName("guild_id"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.HasKey("Code") - .HasName("pk_invites"); - - b.HasIndex("GuildId") - .HasDatabaseName("ix_invites_guild_id"); - - b.ToTable("invites", (string)null); - }); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.Message", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("AttachmentSize") - .HasColumnType("integer") - .HasColumnName("attachment_size"); - - b.Property("ChannelId") - .HasColumnType("bigint") - .HasColumnName("channel_id"); - - b.Property("EncryptedContent") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("content"); - - b.Property("EncryptedMetadata") - .HasColumnType("bytea") - .HasColumnName("metadata"); - - b.Property("EncryptedUsername") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("username"); - - b.Property("GuildId") - .HasColumnType("bigint") - .HasColumnName("guild_id"); - - b.Property("Member") - .HasColumnType("text") - .HasColumnName("member"); - - b.Property("OriginalId") - .HasColumnType("bigint") - .HasColumnName("original_id"); - - b.Property("System") - .HasColumnType("text") - .HasColumnName("system"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_messages"); - - b.ToTable("messages", (string)null); - }); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.Watchlist", b => - { - b.Property("GuildId") - .HasColumnType("bigint") - .HasColumnName("guild_id"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.Property("AddedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("added_at") - .HasDefaultValueSql("now()"); - - b.Property("ModeratorId") - .HasColumnType("bigint") - .HasColumnName("moderator_id"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text") - .HasColumnName("reason"); - - b.HasKey("GuildId", "UserId") - .HasName("pk_watchlists"); - - b.ToTable("watchlists", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Catalogger.Backend/Database/Migrations/20241017130936_AddDashboardTokens.cs b/Catalogger.Backend/Database/Migrations/20241017130936_AddDashboardTokens.cs deleted file mode 100644 index a37ace5..0000000 --- a/Catalogger.Backend/Database/Migrations/20241017130936_AddDashboardTokens.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Catalogger.Backend.Database.Migrations -{ - /// - public partial class AddDashboardTokens : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "api_tokens", - columns: table => new - { - id = table - .Column(type: "integer", nullable: false) - .Annotation( - "Npgsql:ValueGenerationStrategy", - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn - ), - dashboard_token = table.Column(type: "text", nullable: false), - user_id = table.Column(type: "text", nullable: false), - access_token = table.Column(type: "text", nullable: false), - refresh_token = table.Column(type: "text", nullable: true), - expires_at = table.Column( - type: "timestamp with time zone", - nullable: false - ), - }, - constraints: table => - { - table.PrimaryKey("pk_api_tokens", x => x.id); - } - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "api_tokens"); - } - } -} diff --git a/Catalogger.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs b/Catalogger.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs deleted file mode 100644 index 2709829..0000000 --- a/Catalogger.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ /dev/null @@ -1,215 +0,0 @@ -// -using System.Collections.Generic; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Catalogger.Backend.Database.Migrations -{ - [DbContext(typeof(DatabaseContext))] - partial class DatabaseContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.ApiToken", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("text") - .HasColumnName("access_token"); - - b.Property("DashboardToken") - .IsRequired() - .HasColumnType("text") - .HasColumnName("dashboard_token"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires_at"); - - b.Property("RefreshToken") - .HasColumnType("text") - .HasColumnName("refresh_token"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("text") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_api_tokens"); - - b.ToTable("api_tokens", (string)null); - }); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.Guild", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property>("BannedSystems") - .IsRequired() - .HasColumnType("text[]") - .HasColumnName("banned_systems"); - - b.Property("Channels") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("channels"); - - b.Property>("KeyRoles") - .IsRequired() - .HasColumnType("bigint[]") - .HasColumnName("key_roles"); - - b.HasKey("Id") - .HasName("pk_guilds"); - - b.ToTable("guilds", (string)null); - }); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.IgnoredMessage", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.HasKey("Id") - .HasName("pk_ignored_messages"); - - b.ToTable("ignored_messages", (string)null); - }); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.Invite", b => - { - b.Property("Code") - .HasColumnType("text") - .HasColumnName("code"); - - b.Property("GuildId") - .HasColumnType("bigint") - .HasColumnName("guild_id"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.HasKey("Code") - .HasName("pk_invites"); - - b.HasIndex("GuildId") - .HasDatabaseName("ix_invites_guild_id"); - - b.ToTable("invites", (string)null); - }); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.Message", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("AttachmentSize") - .HasColumnType("integer") - .HasColumnName("attachment_size"); - - b.Property("ChannelId") - .HasColumnType("bigint") - .HasColumnName("channel_id"); - - b.Property("EncryptedContent") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("content"); - - b.Property("EncryptedMetadata") - .HasColumnType("bytea") - .HasColumnName("metadata"); - - b.Property("EncryptedUsername") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("username"); - - b.Property("GuildId") - .HasColumnType("bigint") - .HasColumnName("guild_id"); - - b.Property("Member") - .HasColumnType("text") - .HasColumnName("member"); - - b.Property("OriginalId") - .HasColumnType("bigint") - .HasColumnName("original_id"); - - b.Property("System") - .HasColumnType("text") - .HasColumnName("system"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_messages"); - - b.ToTable("messages", (string)null); - }); - - modelBuilder.Entity("Catalogger.Backend.Database.Models.Watchlist", b => - { - b.Property("GuildId") - .HasColumnType("bigint") - .HasColumnName("guild_id"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.Property("AddedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("added_at") - .HasDefaultValueSql("now()"); - - b.Property("ModeratorId") - .HasColumnType("bigint") - .HasColumnName("moderator_id"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text") - .HasColumnName("reason"); - - b.HasKey("GuildId", "UserId") - .HasName("pk_watchlists"); - - b.ToTable("watchlists", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Catalogger.Backend/Database/Queries/QueryExtensions.cs b/Catalogger.Backend/Database/Queries/QueryExtensions.cs deleted file mode 100644 index 457f961..0000000 --- a/Catalogger.Backend/Database/Queries/QueryExtensions.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (C) 2021-present sam (starshines.gay) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -using Catalogger.Backend.Database.Models; -using Catalogger.Backend.Extensions; -using Microsoft.EntityFrameworkCore; -using Remora.Rest.Core; - -namespace Catalogger.Backend.Database.Queries; - -public static class QueryExtensions -{ - public static async ValueTask GetGuildAsync( - this DatabaseContext db, - Snowflake id, - bool shouldTrack = true, - CancellationToken ct = default - ) => await db.GetGuildAsync(id.ToUlong(), shouldTrack, ct); - - public static async ValueTask GetGuildAsync( - this DatabaseContext db, - Optional id, - bool shouldTrack = true, - CancellationToken ct = default - ) => await db.GetGuildAsync(id.ToUlong(), shouldTrack, ct); - - public static async ValueTask GetGuildAsync( - this DatabaseContext db, - ulong id, - bool shouldTrack = true, - CancellationToken ct = default - ) - { - Guild? guild; - if (shouldTrack) - guild = await db.Guilds.FindAsync([id], ct); - else - guild = await db.Guilds.AsNoTracking().FirstOrDefaultAsync(g => g.Id == id, ct); - - if (guild == null) - throw new CataloggerError("Guild not found, was not initialized during guild create"); - return guild; - } -} diff --git a/Catalogger.Backend/Database/Repositories/ApiTokenRepository.cs b/Catalogger.Backend/Database/Repositories/ApiTokenRepository.cs new file mode 100644 index 0000000..25e7782 --- /dev/null +++ b/Catalogger.Backend/Database/Repositories/ApiTokenRepository.cs @@ -0,0 +1,97 @@ +// Copyright (C) 2021-present sam (starshines.gay) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using Catalogger.Backend.Database.Models; +using Dapper; +using NodaTime; + +namespace Catalogger.Backend.Database.Repositories; + +public class ApiTokenRepository(ILogger logger, DatabaseConnection conn, IClock clock) + : IDisposable, + IAsyncDisposable +{ + private readonly ILogger _logger = logger.ForContext(); + + public async Task GetAsync(string token) => + await conn.QueryFirstOrDefaultAsync( + "select * from api_tokens where dashboard_token = @Token and expires_at > @Now", + new { Token = token, Now = clock.GetCurrentInstant() } + ); + + public async Task CreateAsync( + string dashboardToken, + string userId, + string accessToken, + string? refreshToken, + int expiresIn + ) + { + var expiresAt = clock.GetCurrentInstant() + Duration.FromSeconds(expiresIn); + + return await conn.QueryFirstAsync( + """ + insert into api_tokens (dashboard_token, user_id, access_token, refresh_token, expires_at) + values (@dashboardToken, @userId, @accessToken, @refreshToken, @expiresAt) + returning * + """, + new + { + dashboardToken, + userId, + accessToken, + refreshToken, + expiresAt, + } + ); + } + + public async Task UpdateAsync( + int id, + string accessToken, + string? refreshToken, + int expiresIn + ) + { + var expiresAt = clock.GetCurrentInstant() + Duration.FromSeconds(expiresIn); + + return await conn.QueryFirstAsync( + """ + update api_tokens set access_token = @accessToken, refresh_token = @refreshToken, + expires_at = @expiresAt where id = @id + returning * + """, + new + { + id, + accessToken, + refreshToken, + expiresAt, + } + ); + } + + public void Dispose() + { + conn.Dispose(); + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + await conn.DisposeAsync(); + GC.SuppressFinalize(this); + } +} diff --git a/Catalogger.Backend/Database/Dapper/Repositories/GuildRepository.cs b/Catalogger.Backend/Database/Repositories/GuildRepository.cs similarity index 95% rename from Catalogger.Backend/Database/Dapper/Repositories/GuildRepository.cs rename to Catalogger.Backend/Database/Repositories/GuildRepository.cs index 44d39b3..de62b1d 100644 --- a/Catalogger.Backend/Database/Dapper/Repositories/GuildRepository.cs +++ b/Catalogger.Backend/Database/Repositories/GuildRepository.cs @@ -17,7 +17,7 @@ using Catalogger.Backend.Database.Models; using Dapper; using Remora.Rest.Core; -namespace Catalogger.Backend.Database.Dapper.Repositories; +namespace Catalogger.Backend.Database.Repositories; public class GuildRepository(ILogger logger, DatabaseConnection conn) : IDisposable, @@ -89,8 +89,8 @@ public class GuildRepository(ILogger logger, DatabaseConnection conn) public async Task UpdateChannelConfigAsync(Snowflake id, Guild.ChannelConfig config) => await conn.ExecuteAsync( - "update guilds set channels = @Channels where id = @Id", - new { Id = id, Channels = config } + "update guilds set channels = @Channels::jsonb where id = @Id", + new { Id = id.Value, Channels = config } ); public void Dispose() diff --git a/Catalogger.Backend/Database/Dapper/Repositories/InviteRepository.cs b/Catalogger.Backend/Database/Repositories/InviteRepository.cs similarity index 97% rename from Catalogger.Backend/Database/Dapper/Repositories/InviteRepository.cs rename to Catalogger.Backend/Database/Repositories/InviteRepository.cs index 45183de..9ea7382 100644 --- a/Catalogger.Backend/Database/Dapper/Repositories/InviteRepository.cs +++ b/Catalogger.Backend/Database/Repositories/InviteRepository.cs @@ -17,7 +17,7 @@ using Catalogger.Backend.Database.Models; using Dapper; using Remora.Rest.Core; -namespace Catalogger.Backend.Database.Dapper.Repositories; +namespace Catalogger.Backend.Database.Repositories; public class InviteRepository(ILogger logger, DatabaseConnection conn) : IDisposable, diff --git a/Catalogger.Backend/Database/Dapper/Repositories/MessageRepository.cs b/Catalogger.Backend/Database/Repositories/MessageRepository.cs similarity index 99% rename from Catalogger.Backend/Database/Dapper/Repositories/MessageRepository.cs rename to Catalogger.Backend/Database/Repositories/MessageRepository.cs index 7cce3c0..17925c3 100644 --- a/Catalogger.Backend/Database/Dapper/Repositories/MessageRepository.cs +++ b/Catalogger.Backend/Database/Repositories/MessageRepository.cs @@ -20,7 +20,7 @@ using Remora.Discord.API; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Rest.Core; -namespace Catalogger.Backend.Database.Dapper.Repositories; +namespace Catalogger.Backend.Database.Repositories; public class MessageRepository( ILogger logger, diff --git a/Catalogger.Backend/Database/Dapper/Repositories/WatchlistRepository.cs b/Catalogger.Backend/Database/Repositories/WatchlistRepository.cs similarity index 96% rename from Catalogger.Backend/Database/Dapper/Repositories/WatchlistRepository.cs rename to Catalogger.Backend/Database/Repositories/WatchlistRepository.cs index 0f06ab8..8177f78 100644 --- a/Catalogger.Backend/Database/Dapper/Repositories/WatchlistRepository.cs +++ b/Catalogger.Backend/Database/Repositories/WatchlistRepository.cs @@ -17,7 +17,7 @@ using Catalogger.Backend.Database.Models; using Dapper; using Remora.Rest.Core; -namespace Catalogger.Backend.Database.Dapper.Repositories; +namespace Catalogger.Backend.Database.Repositories; public class WatchlistRepository(ILogger logger, DatabaseConnection conn) : IDisposable, diff --git a/Catalogger.Backend/Database/setup_migrations.sql b/Catalogger.Backend/Database/setup_migrations.sql new file mode 100644 index 0000000..d79df46 --- /dev/null +++ b/Catalogger.Backend/Database/setup_migrations.sql @@ -0,0 +1,4 @@ +create table if not exists migrations ( + migration_name text primary key, + applied_at timestamptz not null default (now()) +); diff --git a/Catalogger.Backend/Extensions/StartupExtensions.cs b/Catalogger.Backend/Extensions/StartupExtensions.cs index 32a8ca3..176d821 100644 --- a/Catalogger.Backend/Extensions/StartupExtensions.cs +++ b/Catalogger.Backend/Extensions/StartupExtensions.cs @@ -22,12 +22,9 @@ using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Cache.RedisCache; using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Dapper; -using Catalogger.Backend.Database.Dapper.Repositories; -using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Database.Redis; +using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Services; -using Microsoft.EntityFrameworkCore; using NodaTime; using Prometheus; using Remora.Discord.API; @@ -106,6 +103,7 @@ public static class StartupExtensions services .AddSingleton(SystemClock.Instance) .AddDatabasePool() + .AddScoped() .AddScoped() .AddScoped() .AddScoped() @@ -159,6 +157,7 @@ public static class StartupExtensions return services; return services + .AddScoped() .AddScoped() .AddScoped() .AddScoped() @@ -197,17 +196,8 @@ public static class StartupExtensions DatabasePool.ConfigureDapper(); - await using (var db = scope.ServiceProvider.GetRequiredService()) - { - var migrationCount = (await db.Database.GetPendingMigrationsAsync()).Count(); - if (migrationCount != 0) - { - logger.Information("Applying {Count} database migrations", migrationCount); - await db.Database.MigrateAsync(); - } - else - logger.Information("There are no pending migrations"); - } + await using var migrator = scope.ServiceProvider.GetRequiredService(); + await migrator.Migrate(); var config = scope.ServiceProvider.GetRequiredService(); var slashService = scope.ServiceProvider.GetRequiredService(); diff --git a/Catalogger.Backend/Program.cs b/Catalogger.Backend/Program.cs index 035d669..27710e0 100644 --- a/Catalogger.Backend/Program.cs +++ b/Catalogger.Backend/Program.cs @@ -16,7 +16,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using Catalogger.Backend.Bot.Commands; -using Catalogger.Backend.Database; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Prometheus; @@ -110,8 +109,7 @@ if (!config.Logging.EnableMetrics) builder.Services.AddHostedService(); builder - .Services.AddDbContext() - .MaybeAddDashboardServices(config) + .Services.MaybeAddDashboardServices(config) .MaybeAddRedisCaches(config) .AddCustomServices() .AddEndpointsApiExplorer() diff --git a/Catalogger.Backend/Services/BackgroundTasksService.cs b/Catalogger.Backend/Services/BackgroundTasksService.cs index 936ed45..11a35da 100644 --- a/Catalogger.Backend/Services/BackgroundTasksService.cs +++ b/Catalogger.Backend/Services/BackgroundTasksService.cs @@ -13,8 +13,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using Catalogger.Backend.Database.Dapper.Repositories; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Repositories; namespace Catalogger.Backend.Services; diff --git a/Catalogger.Backend/Services/GuildFetchService.cs b/Catalogger.Backend/Services/GuildFetchService.cs index 040fdea..f843bb1 100644 --- a/Catalogger.Backend/Services/GuildFetchService.cs +++ b/Catalogger.Backend/Services/GuildFetchService.cs @@ -19,7 +19,6 @@ using Catalogger.Backend.Cache; using Humanizer; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Gateway.Commands; -using Remora.Discord.Gateway; using Remora.Rest.Core; namespace Catalogger.Backend.Services; diff --git a/Catalogger.Backend/Services/MetricsCollectionService.cs b/Catalogger.Backend/Services/MetricsCollectionService.cs index cb0ec64..7ac9ffe 100644 --- a/Catalogger.Backend/Services/MetricsCollectionService.cs +++ b/Catalogger.Backend/Services/MetricsCollectionService.cs @@ -16,10 +16,8 @@ using System.Diagnostics; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Dapper; using Dapper; using Humanizer; -using Microsoft.EntityFrameworkCore; using Prometheus; namespace Catalogger.Backend.Services; @@ -40,7 +38,6 @@ public class MetricsCollectionService( var timer = CataloggerMetrics.MetricsCollectionTime.NewTimer(); await using var scope = services.CreateAsyncScope(); - await using var db = scope.ServiceProvider.GetRequiredService(); await using var conn = scope.ServiceProvider.GetRequiredService(); var messageCount = await conn.ExecuteScalarAsync("select count(id) from messages"); diff --git a/Catalogger.Backend/Services/WebhookExecutorService.cs b/Catalogger.Backend/Services/WebhookExecutorService.cs index eed437e..dfde9b2 100644 --- a/Catalogger.Backend/Services/WebhookExecutorService.cs +++ b/Catalogger.Backend/Services/WebhookExecutorService.cs @@ -18,10 +18,10 @@ using System.Diagnostics.CodeAnalysis; using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Extensions; +using OneOf; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.API.Objects; using Remora.Rest.Core; using Guild = Catalogger.Backend.Database.Models.Guild; @@ -109,7 +109,7 @@ public class WebhookExecutorService( } var attachments = files - .Select>(f => f) + .Select>(f => f) .ToList(); if (embeds.Count == 0 && attachments.Count == 0) diff --git a/Catalogger.Backend/packages.lock.json b/Catalogger.Backend/packages.lock.json index cfc0acc..2af0ac1 100644 --- a/Catalogger.Backend/packages.lock.json +++ b/Catalogger.Backend/packages.lock.json @@ -8,27 +8,6 @@ "resolved": "2.1.35", "contentHash": "YKRwjVfrG7GYOovlGyQoMvr1/IJdn+7QzNXJxyMh0YfFF5yvDmTYaJOVYWsckreNjGsGSEtrMTpnzxTUq/tZQw==" }, - "EFCore.NamingConventions": { - "type": "Direct", - "requested": "[8.0.3, )", - "resolved": "8.0.3", - "contentHash": "TdDarM6kyIS2oVIhrs3W+r+xL/76ooFJxIXxfhzsNJQu0pB9VdFZwuyKvKJnhoc7OHYFNTBP08AN37kr4CPc+Q==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[8.0.0, 9.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[8.0.0, 9.0.0)", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" - } - }, - "EntityFrameworkCore.Exceptions.PostgreSQL": { - "type": "Direct", - "requested": "[8.1.3, )", - "resolved": "8.1.3", - "contentHash": "hqTsPy2SeupzawK/AeH5/8/K7+KEdZjQbyKVlxBX45ei86eU8D14X9E06uS6MX+J5TdSKY6o+WXGS7G2k8Xvyw==", - "dependencies": { - "EntityFrameworkCore.Exceptions.Common": "8.1.3", - "Npgsql": "8.0.3" - } - }, "Humanizer.Core": { "type": "Direct", "requested": "[2.14.1, )", @@ -65,31 +44,6 @@ "Microsoft.OpenApi": "1.4.3" } }, - "Microsoft.EntityFrameworkCore": { - "type": "Direct", - "requested": "[8.0.8, )", - "resolved": "8.0.8", - "contentHash": "iK+jrJzkfbIxutB7or808BPmJtjUEi5O+eSM7cLDwsyde6+3iOujCSfWnrHrLxY3u+EQrJD+aD8DJ6ogPA2Rtw==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "8.0.8", - "Microsoft.EntityFrameworkCore.Analyzers": "8.0.8", - "Microsoft.Extensions.Caching.Memory": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0" - } - }, - "Microsoft.EntityFrameworkCore.Design": { - "type": "Direct", - "requested": "[8.0.8, )", - "resolved": "8.0.8", - "contentHash": "MmQAMHdjZR8Iyn/FVQrh9weJQTn0HqtKa3vELS9ffQJat/qXgnTam9M9jqvePphjkYp5Scee+Hy+EJR4nmWmOA==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.5.0", - "Microsoft.EntityFrameworkCore.Relational": "8.0.8", - "Microsoft.Extensions.DependencyModel": "8.0.1", - "Mono.TextTemplating": "2.2.1" - } - }, "Newtonsoft.Json": { "type": "Direct", "requested": "[13.0.3, )", @@ -123,28 +77,6 @@ "Microsoft.Extensions.Logging.Abstractions": "8.0.0" } }, - "Npgsql.EntityFrameworkCore.PostgreSQL": { - "type": "Direct", - "requested": "[8.0.8, )", - "resolved": "8.0.8", - "contentHash": "D5WWJZJTgZYUmGv66BARXbTlinp2a5f5RueJqGYoHuWJw02J0i2va/RA+8N4A5hLORK5YKMRqXhFWtKsZdrksw==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "8.0.8", - "Microsoft.EntityFrameworkCore.Abstractions": "8.0.8", - "Microsoft.EntityFrameworkCore.Relational": "8.0.8", - "Npgsql": "8.0.4" - } - }, - "Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime": { - "type": "Direct", - "requested": "[8.0.8, )", - "resolved": "8.0.8", - "contentHash": "lq+0sHuHySoYb6AxluUN+4TMrLTNOL4f5kKtf8eWoN0IoTC8cTL6IXOhfTOI8klUOdnOTMx3f7mdDKdR5AV1Jw==", - "dependencies": { - "Npgsql.EntityFrameworkCore.PostgreSQL": "8.0.8", - "Npgsql.NodaTime": "8.0.4" - } - }, "Npgsql.NodaTime": { "type": "Direct", "requested": "[8.0.5, )", @@ -291,14 +223,6 @@ "resolved": "8.2.2", "contentHash": "+zIp8d3sbtYaRbM6hqDs4Ui/z34j7DcUmleruZlYLE4CVxXq+MO8XJyIs42vzeTYFX+k0Iq1dEbBUnQ4z/Gnrw==" }, - "EntityFrameworkCore.Exceptions.Common": { - "type": "Transitive", - "resolved": "8.1.3", - "contentHash": "nweeiVHx4HbDi6+TqendOe0QmN0a9v0AB5FaL83eToqFFztwGIhOqLfveKqJDYSCU51CJShW8kbU1onZLdZZSg==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "8.0.0" - } - }, "FuzzySharp": { "type": "Transitive", "resolved": "2.0.2", @@ -318,84 +242,11 @@ "Newtonsoft.Json": "13.0.3" } }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "3.3.3", - "contentHash": "j/rOZtLMVJjrfLRlAMckJLPW/1rze9MT1yfWqSIbUPGRu1m1P0fuo9PmqapwsmePfGB5PJrudQLvmUOAMF0DqQ==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "lwAbIZNdnY0SUNoDmZHkVUwLO8UyNnyyh1t/4XsbFxi4Ounb3xszIYZaWhyj5ZjyfcwqwmtMbE7fUTVCqQEIdQ==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.3", - "System.Collections.Immutable": "6.0.0", - "System.Reflection.Metadata": "6.0.1", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encoding.CodePages": "6.0.0" - } - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "cM59oMKAOxvdv76bdmaKPy5hfj+oR+zxikWoueEB7CwTko7mt9sVKZI8Qxlov0C/LuKEG+WQwifepqL3vuTiBQ==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "[4.5.0]" - } - }, - "Microsoft.CodeAnalysis.CSharp.Workspaces": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "h74wTpmGOp4yS4hj+EvNzEiPgg/KVs2wmSfTZ81upJZOtPkJsVkgfsgtxxqmAeapjT/vLKfmYV0bS8n5MNVP+g==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.CodeAnalysis.CSharp": "[4.5.0]", - "Microsoft.CodeAnalysis.Common": "[4.5.0]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.5.0]" - } - }, - "Microsoft.CodeAnalysis.Workspaces.Common": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "l4dDRmGELXG72XZaonnOeORyD/T5RpEu5LGHOUIhnv+MmUWDY/m1kWXGwtcgQ5CJ5ynkFiRnIYzTKXYjUs7rbw==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.Bcl.AsyncInterfaces": "6.0.0", - "Microsoft.CodeAnalysis.Common": "[4.5.0]", - "System.Composition": "6.0.0", - "System.IO.Pipelines": "6.0.3", - "System.Threading.Channels": "6.0.0" - } - }, "Microsoft.CSharp": { "type": "Transitive", "resolved": "4.7.0", "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "8.0.8", - "contentHash": "9mMQkZsfL1c2iifBD8MWRmwy59rvsVtR9NOezJj7+g1j4P7g49MJHd8k8faC/v7d5KuHkQ6KOQiSItvoRt9PXA==" - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Transitive", - "resolved": "8.0.8", - "contentHash": "OlAXMU+VQgLz5y5/SBkLvAa9VeiR3dlJqgIebEEH2M2NGA3evm68/Tv7SLWmSxwnEAtA3nmDEZF2pacK6eXh4Q==" - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Transitive", - "resolved": "8.0.8", - "contentHash": "3WnrwdXxKg4L98cDx0lNEEau8U2lsfuBJCs0Yzht+5XVTmahboM7MukKfQHAzVsHUPszm6ci929S7Qas0WfVHA==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "8.0.8", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" - } - }, "Microsoft.Extensions.ApiDescription.Server": { "type": "Transitive", "resolved": "6.0.5", @@ -585,14 +436,6 @@ "resolved": "1.6.14", "contentHash": "tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==" }, - "Mono.TextTemplating": { - "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "KZYeKBET/2Z0gY1WlTAK7+RHTl7GSbtvTLDXEZZojUdAPqpQNDL6tHv7VUpqfX5VEOh+uRGKaZXkuD253nEOBQ==", - "dependencies": { - "System.CodeDom": "4.4.0" - } - }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", @@ -860,72 +703,11 @@ "resolved": "6.8.1", "contentHash": "lpEszYJ7vZaTTE5Dp8MrsbSHrgDfjhDMjzW1qOA1Xs1Dnj3ZRBJAcPZUTsa5Bva+nLaw91JJ8OI8FkSg8hhIyA==" }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "2sCCb7doXEwtYAbqzbF/8UAeDRMNmPaQbU2q50Psg1J9KzumyVVCgKQY8s53WIPTufNT0DpSe9QRvVjOzfDWBA==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, "System.ComponentModel.Annotations": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" }, - "System.Composition": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "d7wMuKQtfsxUa7S13tITC8n1cQzewuhD5iDjZtK2prwFfKVzdYtgrTHgjaV03Zq7feGQ5gkP85tJJntXwInsJA==", - "dependencies": { - "System.Composition.AttributedModel": "6.0.0", - "System.Composition.Convention": "6.0.0", - "System.Composition.Hosting": "6.0.0", - "System.Composition.Runtime": "6.0.0", - "System.Composition.TypedParts": "6.0.0" - } - }, - "System.Composition.AttributedModel": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "WK1nSDLByK/4VoC7fkNiFuTVEiperuCN/Hyn+VN30R+W2ijO1d0Z2Qm0ScEl9xkSn1G2MyapJi8xpf4R8WRa/w==" - }, - "System.Composition.Convention": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "XYi4lPRdu5bM4JVJ3/UIHAiG6V6lWWUlkhB9ab4IOq0FrRsp0F4wTyV4Dj+Ds+efoXJ3qbLqlvaUozDO7OLeXA==", - "dependencies": { - "System.Composition.AttributedModel": "6.0.0" - } - }, - "System.Composition.Hosting": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "w/wXjj7kvxuHPLdzZ0PAUt++qJl03t7lENmb2Oev0n3zbxyNULbWBlnd5J5WUMMv15kg5o+/TCZFb6lSwfaUUQ==", - "dependencies": { - "System.Composition.Runtime": "6.0.0" - } - }, - "System.Composition.Runtime": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "qkRH/YBaMPTnzxrS5RDk1juvqed4A6HOD/CwRcDGyPpYps1J27waBddiiq1y93jk2ZZ9wuA/kynM+NO0kb3PKg==" - }, - "System.Composition.TypedParts": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "iUR1eHrL8Cwd82neQCJ00MpwNIBs4NZgXzrPqx8NJf/k4+mwBO0XCRmHYJT4OLSwDDqh5nBLJWkz5cROnrGhRA==", - "dependencies": { - "System.Composition.AttributedModel": "6.0.0", - "System.Composition.Hosting": "6.0.0", - "System.Composition.Runtime": "6.0.0" - } - }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "8.0.0", @@ -933,29 +715,13 @@ }, "System.IO.Pipelines": { "type": "Transitive", - "resolved": "6.0.3", - "contentHash": "ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "III/lNMSn0ZRBuM9m5Cgbiho5j81u0FAEagFX5ta2DKbljZ3T0IpD8j+BIiHQPeKqJppWS9bGEp6JnKnWKze0g==", - "dependencies": { - "System.Collections.Immutable": "6.0.0" - } + "resolved": "5.0.1", + "contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg==" }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } + "resolved": "4.7.1", + "contentHash": "zOHkQmzPCn5zm/BH+cxC1XbUS3P4Yoi3xzW7eRgVpDR2tPGSzyMZ17Ig1iRkfJuY0nhxkQQde8pgePNiA7z7TQ==" }, "System.Text.Encodings.Web": { "type": "Transitive",