commit 0a579154478d3e354bf2f371f98df1ab44429aa6 Author: edraft Date: Mon Jan 5 21:54:15 2026 +0100 Added db migrations diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8382607 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +obj/ +Debug/ + +.idea/ +.vscode/ +*.suo +*.user +*.userosscache +*.sln.docstates \ No newline at end of file diff --git a/api/dc_bot.API/.dockerignore b/api/dc_bot.API/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/api/dc_bot.API/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/api/dc_bot.API/dc_bot.API.sln b/api/dc_bot.API/dc_bot.API.sln new file mode 100644 index 0000000..6318c39 --- /dev/null +++ b/api/dc_bot.API/dc_bot.API.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dc_bot.API", "dc_bot.API\dc_bot.API.csproj", "{473D45A5-BC19-4DC3-A2A2-E57B75C6C0F2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dc_bot.db", "dc_bot.db\dc_bot.db.csproj", "{BFAA7A97-736C-4E78-B83C-2F97D4E6797F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "utils", "utils\utils.csproj", "{3BF8EE60-57BA-4526-AC5A-47E3149CEDB4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {473D45A5-BC19-4DC3-A2A2-E57B75C6C0F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {473D45A5-BC19-4DC3-A2A2-E57B75C6C0F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {473D45A5-BC19-4DC3-A2A2-E57B75C6C0F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {473D45A5-BC19-4DC3-A2A2-E57B75C6C0F2}.Release|Any CPU.Build.0 = Release|Any CPU + {BFAA7A97-736C-4E78-B83C-2F97D4E6797F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BFAA7A97-736C-4E78-B83C-2F97D4E6797F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BFAA7A97-736C-4E78-B83C-2F97D4E6797F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BFAA7A97-736C-4E78-B83C-2F97D4E6797F}.Release|Any CPU.Build.0 = Release|Any CPU + {3BF8EE60-57BA-4526-AC5A-47E3149CEDB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BF8EE60-57BA-4526-AC5A-47E3149CEDB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BF8EE60-57BA-4526-AC5A-47E3149CEDB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BF8EE60-57BA-4526-AC5A-47E3149CEDB4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/api/dc_bot.API/dc_bot.API/Dockerfile b/api/dc_bot.API/dc_bot.API/Dockerfile new file mode 100644 index 0000000..58d9d23 --- /dev/null +++ b/api/dc_bot.API/dc_bot.API/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["dc_bot.API/dc_bot.API.csproj", "dc_bot.API/"] +RUN dotnet restore "dc_bot.API/dc_bot.API.csproj" +COPY . . +WORKDIR "/src/dc_bot.API" +RUN dotnet build "./dc_bot.API.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./dc_bot.API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "dc_bot.API.dll"] diff --git a/api/dc_bot.API/dc_bot.API/Program.cs b/api/dc_bot.API/dc_bot.API/Program.cs new file mode 100644 index 0000000..2d6e2ac --- /dev/null +++ b/api/dc_bot.API/dc_bot.API/Program.cs @@ -0,0 +1,34 @@ +using dc_bot.db; +using Microsoft.EntityFrameworkCore; +using utils; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDbContextPool(opt => + opt.UseNpgsql(ConnectionStringHelper.GetConnectionString())); + +builder.Services.AddScoped(); + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +using (var scope = app.Services.CreateScope()) +{ + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.EnsureCreated(); + var migrationService = scope.ServiceProvider.GetRequiredService(); + await migrationService.Migrate(); +} + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.Run(); \ No newline at end of file diff --git a/api/dc_bot.API/dc_bot.API/Properties/launchSettings.json b/api/dc_bot.API/dc_bot.API/Properties/launchSettings.json new file mode 100644 index 0000000..f6c7e43 --- /dev/null +++ b/api/dc_bot.API/dc_bot.API/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5002", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DB_HOST": "localhost", + "DB_PORT": "5438", + "DB_NAME": "dc_bot", + "DB_USER": "dc_bot", + "DB_PASSWORD": "dc_bot" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7067;http://localhost:5002", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/api/dc_bot.API/dc_bot.API/appsettings.Development.json b/api/dc_bot.API/dc_bot.API/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/api/dc_bot.API/dc_bot.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/api/dc_bot.API/dc_bot.API/appsettings.json b/api/dc_bot.API/dc_bot.API/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/api/dc_bot.API/dc_bot.API/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/api/dc_bot.API/dc_bot.API/dc_bot.API.csproj b/api/dc_bot.API/dc_bot.API/dc_bot.API.csproj new file mode 100644 index 0000000..3d66517 --- /dev/null +++ b/api/dc_bot.API/dc_bot.API/dc_bot.API.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + Linux + + + + + + + + + + + .dockerignore + + + + + + + + + diff --git a/api/dc_bot.API/dc_bot.db/AppDbContext.cs b/api/dc_bot.API/dc_bot.db/AppDbContext.cs new file mode 100644 index 0000000..51246de --- /dev/null +++ b/api/dc_bot.API/dc_bot.db/AppDbContext.cs @@ -0,0 +1,26 @@ +using dc_bot.db.Model; +using dc_bot.db.Model.administration; +using dc_bot.db.Model.system; +using Microsoft.EntityFrameworkCore; + +namespace dc_bot.db; + +public class AppDbContext(DbContextOptions options) : DbContext(options) +{ + public required DbSet ExecutedMigrations { get; set; } + public required DbSet Users { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(e => + { + e.ToTable("_executed_migrations", "system"); + e.HasKey(e => e.MigrationId); + }); + + modelBuilder.Entity(e => + { + e.ToTable("users", "administration"); + }); + } +} \ No newline at end of file diff --git a/api/dc_bot.API/dc_bot.db/MigrationService.cs b/api/dc_bot.API/dc_bot.db/MigrationService.cs new file mode 100644 index 0000000..fbaf948 --- /dev/null +++ b/api/dc_bot.API/dc_bot.db/MigrationService.cs @@ -0,0 +1,74 @@ +using System.Runtime.CompilerServices; +using dc_bot.db.Model.system; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace dc_bot.db; + +public class MigrationService +{ + private readonly IServiceScopeFactory _scopeFactory; + + private readonly string MigrationDirectory = Environment.GetEnvironmentVariable("DB_MIGRATION_DIR") ?? + Path.Combine(AppContext.BaseDirectory, "Scripts"); + + public MigrationService(IServiceScopeFactory scopeFactory) + { + _scopeFactory = scopeFactory; + } + + private List GetMigrations() + { + if (!Directory.Exists(MigrationDirectory)) + return new List(); + + var files = Directory.GetFiles(MigrationDirectory, "*.sql") + .Select(Path.GetFileName) + .OrderBy(f => f) + .ToList(); + return files; + } + + public async Task Migrate() + { + using var scope = _scopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + var appliedMigrations = await context.ExecutedMigrations + .Select(m => m.MigrationId) + .ToListAsync(); + + var appliedMigrationNames = appliedMigrations + .Select(m => m.Replace(".sql", string.Empty)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var migrations = GetMigrations() + .Where(m => !appliedMigrationNames.Contains(m)) + .ToList(); + + foreach (var migration in migrations) + { + try + { + var migrationPath = Path.Combine(MigrationDirectory, migration); + var sql = await File.ReadAllTextAsync(migrationPath); + + await context.Database.ExecuteSqlRawAsync(sql); + + var executedMigration = new ExecutedMigration + { + MigrationId = migration, + Created = DateTime.UtcNow, + Updated = DateTime.UtcNow + }; + context.ExecutedMigrations.Add(executedMigration); + await context.SaveChangesAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"Error applying migration {migration}: {ex.Message}"); + throw; + } + } + } +} \ No newline at end of file diff --git a/api/dc_bot.API/dc_bot.db/Model/IDbModel.cs b/api/dc_bot.API/dc_bot.db/Model/IDbModel.cs new file mode 100644 index 0000000..c2a7d49 --- /dev/null +++ b/api/dc_bot.API/dc_bot.db/Model/IDbModel.cs @@ -0,0 +1,9 @@ +namespace dc_bot.db.Model; + +public interface IDbModel +{ + public bool Deleted { get; set; } + public Double EditorId { get; set; } + public DateTimeOffset Created { get; set; } + public DateTimeOffset Updated { get; set; } +} \ No newline at end of file diff --git a/api/dc_bot.API/dc_bot.db/Model/administration/User.cs b/api/dc_bot.API/dc_bot.db/Model/administration/User.cs new file mode 100644 index 0000000..6d14960 --- /dev/null +++ b/api/dc_bot.API/dc_bot.db/Model/administration/User.cs @@ -0,0 +1,12 @@ +namespace dc_bot.db.Model.administration; + +public class User : IDbModel +{ + public Double Id { get; set; } + public required string KeycloakId { get; init; } + + public bool Deleted { get; set; } + public double EditorId { get; set; } + public DateTimeOffset Created { get; set; } + public DateTimeOffset Updated { get; set; } +} \ No newline at end of file diff --git a/api/dc_bot.API/dc_bot.db/Model/system/ExecutedMigration.cs b/api/dc_bot.API/dc_bot.db/Model/system/ExecutedMigration.cs new file mode 100644 index 0000000..e93ae0b --- /dev/null +++ b/api/dc_bot.API/dc_bot.db/Model/system/ExecutedMigration.cs @@ -0,0 +1,10 @@ +using Microsoft.EntityFrameworkCore; + +namespace dc_bot.db.Model.system; + +public class ExecutedMigration +{ + public required string MigrationId { get; set; } + public DateTimeOffset Created { get; set; } + public DateTimeOffset Updated { get; set; } +} \ No newline at end of file diff --git a/api/dc_bot.API/dc_bot.db/Scripts/2026-01-05-21-05-inital-migration.sql b/api/dc_bot.API/dc_bot.db/Scripts/2026-01-05-21-05-inital-migration.sql new file mode 100644 index 0000000..4107afe --- /dev/null +++ b/api/dc_bot.API/dc_bot.db/Scripts/2026-01-05-21-05-inital-migration.sql @@ -0,0 +1,9 @@ +CREATE SCHEMA IF NOT EXISTS public; +CREATE SCHEMA IF NOT EXISTS system; + +CREATE TABLE IF NOT EXISTS system._executed_migrations +( + MigrationId VARCHAR(255) PRIMARY KEY, + Created timestamptz NOT NULL DEFAULT NOW(), + Updated timestamptz NOT NULL DEFAULT NOW() +); \ No newline at end of file diff --git a/api/dc_bot.API/dc_bot.db/Scripts/2026-01-05-21-10-inital-trigger.sql b/api/dc_bot.API/dc_bot.db/Scripts/2026-01-05-21-10-inital-trigger.sql new file mode 100644 index 0000000..bb86572 --- /dev/null +++ b/api/dc_bot.API/dc_bot.db/Scripts/2026-01-05-21-10-inital-trigger.sql @@ -0,0 +1,37 @@ +CREATE OR REPLACE FUNCTION public.history_trigger_function() + RETURNS TRIGGER AS +$$ +DECLARE + schema_name TEXT; + history_table_name TEXT; +BEGIN + -- Construct the name of the history table based on the current table + schema_name := TG_TABLE_SCHEMA; + history_table_name := TG_TABLE_NAME || '_history'; + + IF (TG_OP = 'INSERT') THEN + RETURN NEW; + END IF; + + -- Insert the old row into the history table on UPDATE or DELETE + IF (TG_OP = 'UPDATE' OR TG_OP = 'DELETE') THEN + EXECUTE format( + 'INSERT INTO %I.%I SELECT ($1).*', + schema_name, + history_table_name + ) + USING OLD; + END IF; + + -- For UPDATE, update the UpdatedUtc column and return the new row + IF (TG_OP = 'UPDATE') THEN + NEW.updated := NOW(); -- Update the UpdatedUtc column + RETURN NEW; + END IF; + + -- For DELETE, return OLD to allow the deletion + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + END IF; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/api/dc_bot.API/dc_bot.db/Scripts/2026-01-05-21-15-inital-model.sql b/api/dc_bot.API/dc_bot.db/Scripts/2026-01-05-21-15-inital-model.sql new file mode 100644 index 0000000..e918e0e --- /dev/null +++ b/api/dc_bot.API/dc_bot.db/Scripts/2026-01-05-21-15-inital-model.sql @@ -0,0 +1,26 @@ +CREATE SCHEMA IF NOT EXISTS administration; + +CREATE TABLE IF NOT EXISTS administration.users +( + Id SERIAL PRIMARY KEY, + KeycloakId UUID NOT NULL, + -- for history + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + EditorId INT NULL REFERENCES administration.users (Id), + Created timestamptz NOT NULL DEFAULT NOW(), + Updated timestamptz NOT NULL DEFAULT NOW(), + + CONSTRAINT UC_KeycloakId UNIQUE (KeycloakId) +); + +CREATE TABLE IF NOT EXISTS administration.users_history +( + LIKE administration.users +); + +CREATE TRIGGER users_history_trigger + BEFORE INSERT OR UPDATE OR DELETE + ON administration.users + FOR EACH ROW +EXECUTE FUNCTION public.history_trigger_function(); + diff --git a/api/dc_bot.API/dc_bot.db/dc_bot.db.csproj b/api/dc_bot.API/dc_bot.db/dc_bot.db.csproj new file mode 100644 index 0000000..34b5fce --- /dev/null +++ b/api/dc_bot.API/dc_bot.db/dc_bot.db.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/api/dc_bot.API/utils/ConnectionStringHelper.cs b/api/dc_bot.API/utils/ConnectionStringHelper.cs new file mode 100644 index 0000000..4337540 --- /dev/null +++ b/api/dc_bot.API/utils/ConnectionStringHelper.cs @@ -0,0 +1,15 @@ +namespace utils; + +public static class ConnectionStringHelper +{ + public static string GetConnectionString() + { + var host = Environment.GetEnvironmentVariable("DB_HOST") ?? "localhost"; + var port = Environment.GetEnvironmentVariable("DB_PORT") ?? "5432"; + var database = Environment.GetEnvironmentVariable("DB_NAME") ?? "dc_bot"; + var username = Environment.GetEnvironmentVariable("DB_USER") ?? "postgres"; + var password = Environment.GetEnvironmentVariable("DB_PASSWORD") ?? "password"; + + return $"Host={host};Port={port};Database={database};Username={username};Password={password}"; + } +} \ No newline at end of file diff --git a/api/dc_bot.API/utils/utils.csproj b/api/dc_bot.API/utils/utils.csproj new file mode 100644 index 0000000..237d661 --- /dev/null +++ b/api/dc_bot.API/utils/utils.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + +