From fcd2dca8dcd87ae3f2aa18209b0c7a0d705fa3e0 Mon Sep 17 00:00:00 2001 From: Jonas <77726472+kobolol@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:40:42 +0200 Subject: [PATCH] Integrate PostgreSQL with EF Core & migrations Add PostgreSQL support and EF Core migrations for local development. Introduces ApplicationDbContext, adds Npgsql.EntityFrameworkCore.PostgreSQL and EF Core Design packages, and includes generated migrations (InitialPostgres and RemoveTestItems) plus updated model snapshot. Program.cs now loads an optional API/appsettings.custom.json, configures the DbContext from ConnectionStrings:Postgres and runs Database.Migrate() on startup. Adds a Dev docker-compose.yml to run postgres + pgAdmin, pins dotnet-ef in dotnet-tools.json, and adds ConnectionStrings to appsettings files. Also ignores API/appsettings.custom.json in .gitignore and updates README/codexInfo to document the new DB/dev workflow. --- .gitignore | 2 + API/API.csproj | 5 ++ API/Database/ApplicationDbContext.cs | 5 ++ API/Dev/docker-compose.yml | 38 ++++++++++++ ...20260418134949_InitialPostgres.Designer.cs | 58 +++++++++++++++++++ .../20260418134949_InitialPostgres.cs | 39 +++++++++++++ ...20260418153650_RemoveTestItems.Designer.cs | 29 ++++++++++ .../20260418153650_RemoveTestItems.cs | 39 +++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 26 +++++++++ API/Program.cs | 18 ++++++ API/appsettings.Development.json | 3 + API/appsettings.json | 3 + README.md | 2 +- codexInfo.md | 14 +++++ dotnet-tools.json | 13 +++++ 15 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 API/Database/ApplicationDbContext.cs create mode 100644 API/Dev/docker-compose.yml create mode 100644 API/Migrations/20260418134949_InitialPostgres.Designer.cs create mode 100644 API/Migrations/20260418134949_InitialPostgres.cs create mode 100644 API/Migrations/20260418153650_RemoveTestItems.Designer.cs create mode 100644 API/Migrations/20260418153650_RemoveTestItems.cs create mode 100644 API/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 dotnet-tools.json diff --git a/.gitignore b/.gitignore index 0e5c9aa..bbc75d1 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,8 @@ __screenshots__/ .env.local .env.*.local *.local +API/appsettings.custom.json +API/appsettings.custom.*.json # Temporary files ~$* diff --git a/API/API.csproj b/API/API.csproj index 58e40dc..6d308c2 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -7,6 +7,11 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/API/Database/ApplicationDbContext.cs b/API/Database/ApplicationDbContext.cs new file mode 100644 index 0000000..5465878 --- /dev/null +++ b/API/Database/ApplicationDbContext.cs @@ -0,0 +1,5 @@ +using Microsoft.EntityFrameworkCore; + +namespace API.Database; + +public class ApplicationDbContext(DbContextOptions options) : DbContext(options); diff --git a/API/Dev/docker-compose.yml b/API/Dev/docker-compose.yml new file mode 100644 index 0000000..b41e3a8 --- /dev/null +++ b/API/Dev/docker-compose.yml @@ -0,0 +1,38 @@ +services: + postgres: + image: postgres:16-alpine + container_name: hoard-postgres + restart: unless-stopped + environment: + POSTGRES_DB: hoard + POSTGRES_USER: hoard + POSTGRES_PASSWORD: hoard_dev_password + ports: + - "5432:5432" + volumes: + - hoard_postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U hoard -d hoard"] + interval: 10s + timeout: 5s + retries: 5 + + pgadmin: + image: dpage/pgadmin4:9.3 + container_name: hoard-pgadmin + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: familiehimmelberg@gmail.com + PGADMIN_DEFAULT_PASSWORD: admin + PGADMIN_CONFIG_SERVER_MODE: "False" + ports: + - "5050:80" + depends_on: + postgres: + condition: service_healthy + volumes: + - hoard_pgadmin_data:/var/lib/pgadmin + +volumes: + hoard_postgres_data: + hoard_pgadmin_data: diff --git a/API/Migrations/20260418134949_InitialPostgres.Designer.cs b/API/Migrations/20260418134949_InitialPostgres.Designer.cs new file mode 100644 index 0000000..44f7c88 --- /dev/null +++ b/API/Migrations/20260418134949_InitialPostgres.Designer.cs @@ -0,0 +1,58 @@ +// +using System; +using API.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260418134949_InitialPostgres")] + partial class InitialPostgres + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("API.Models.Test.TestItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("test_items", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Migrations/20260418134949_InitialPostgres.cs b/API/Migrations/20260418134949_InitialPostgres.cs new file mode 100644 index 0000000..36c25e1 --- /dev/null +++ b/API/Migrations/20260418134949_InitialPostgres.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace API.Migrations +{ + /// + public partial class InitialPostgres : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "test_items", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + CreatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_test_items", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "test_items"); + } + } +} diff --git a/API/Migrations/20260418153650_RemoveTestItems.Designer.cs b/API/Migrations/20260418153650_RemoveTestItems.Designer.cs new file mode 100644 index 0000000..4d3ef64 --- /dev/null +++ b/API/Migrations/20260418153650_RemoveTestItems.Designer.cs @@ -0,0 +1,29 @@ +// +using API.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260418153650_RemoveTestItems")] + partial class RemoveTestItems + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Migrations/20260418153650_RemoveTestItems.cs b/API/Migrations/20260418153650_RemoveTestItems.cs new file mode 100644 index 0000000..48bb8f0 --- /dev/null +++ b/API/Migrations/20260418153650_RemoveTestItems.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace API.Migrations +{ + /// + public partial class RemoveTestItems : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "test_items"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "test_items", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false), + Description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + UpdatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_test_items", x => x.Id); + }); + } + } +} diff --git a/API/Migrations/ApplicationDbContextModelSnapshot.cs b/API/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..b22d9fb --- /dev/null +++ b/API/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,26 @@ +// +using API.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Program.cs b/API/Program.cs index 0028f4f..55cb5c1 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,10 +1,28 @@ +using API.Database; +using Microsoft.EntityFrameworkCore; + var builder = WebApplication.CreateBuilder(args); +builder.Configuration.AddJsonFile("appsettings.custom.json", optional: true, reloadOnChange: true); + +var connectionString = builder.Configuration.GetConnectionString("Postgres") + ?? throw new InvalidOperationException("Connection string 'Postgres' wurde nicht gefunden."); + +builder.Services.AddDbContext(options => +{ + options.UseNpgsql(connectionString); +}); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); + +using (var scope = app.Services.CreateScope()) +{ + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Database.Migrate(); +} var webRootPath = app.Environment.WebRootPath ?? Path.Combine(app.Environment.ContentRootPath, "wwwroot"); var indexFilePath = Path.Combine(webRootPath, "index.html"); diff --git a/API/appsettings.Development.json b/API/appsettings.Development.json index 0c208ae..2ea6405 100644 --- a/API/appsettings.Development.json +++ b/API/appsettings.Development.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "Postgres": "Host=localhost;Port=5432;Database=hoard;Username=hoard;Password=hoard_dev_password" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/API/appsettings.json b/API/appsettings.json index 10f68b8..411e4a4 100644 --- a/API/appsettings.json +++ b/API/appsettings.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "Postgres": "" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/README.md b/README.md index c0ef714..1353d49 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Hoard

- +

Hoard ist eine einfache, selbst gehostete Web-App zur Verwaltung von Dateien und Ordnern – mit integrierter Markdown-Bearbeitung direkt im Browser. diff --git a/codexInfo.md b/codexInfo.md index 162f83f..ce45853 100644 --- a/codexInfo.md +++ b/codexInfo.md @@ -90,6 +90,11 @@ Ich baue alleine neben meiner Ausbildung eine einfache self-hosted Web-App für - Backend-API ist auf ein Minimal-Setup reduziert und stellt aktuell den Test-Endpunkt `GET /api/health` bereit. - Swagger/OpenAPI ist im Backend nur im Development-Modus aktiv (`/swagger`). - Frontend-Build (`npm run build` im `GUI`-Projekt) schreibt direkt nach `API/wwwroot`; das Backend liefert die SPA und statische Assets aus. +- Backend nutzt jetzt PostgreSQL über `ConnectionStrings:Postgres` mit EF Core (`ApplicationDbContext`) und führt Migrationen beim Start automatisch aus. +- Temporäre Test-Entity und Test-CRUD-Endpunkt (`api/test-items`) wurden wieder entfernt; aktuell bleibt der minimale Health-Endpunkt `GET /api/health`. +- Für lokale Entwicklung liegt unter `API/Dev/docker-compose.yml` ein Stack mit PostgreSQL (`localhost:5432`) und pgAdmin (`localhost:5050`). +- API lädt optional `API/appsettings.custom.json`; wenn vorhanden, überschreibt sie Werte aus `appsettings.json`. +- `API/appsettings.custom.json` ist in `.gitignore` hinterlegt, damit lokale Konfigurationswerte nicht versehentlich committed werden. ## Änderungen durch Codex - Grundlegender UI-Neuaufbau der App-Shell (`GUI/src/Layout.vue`) inklusive Navigation, Footer und Seitenkontext. @@ -111,3 +116,12 @@ Ich baue alleine neben meiner Ausbildung eine einfache self-hosted Web-App für - `API/Program.cs` erweitert, damit statische Dateien aus `wwwroot` inkl. SPA-Fallback (`index.html`) ausgeliefert werden. - SPA-Fallback im Backend aufgeteilt: Frontend-Routen liefern `index.html`, unbekannte `/api/*`-Routen bleiben korrekt `404` statt auf die SPA zu fallen. - Swagger im Backend ergänzt: `Swashbuckle.AspNetCore` eingebunden, Services registriert und UI nur in `Development` aktiviert. +- PostgreSQL-Integration im Backend umgesetzt: `Npgsql.EntityFrameworkCore.PostgreSQL` + `Microsoft.EntityFrameworkCore.Design` in `API/API.csproj` ergänzt und Connection String `ConnectionStrings:Postgres` in den Settings hinterlegt. +- `API/Database/ApplicationDbContext.cs` mit Test-Entity `API/Models/Test/TestItem.cs` angelegt; erste Migrationen in `API/Migrations` erstellt. +- `API/Program.cs` um `AddDbContext` (Postgres) und `Database.Migrate()` beim Start erweitert, damit Migrationen automatisch angewendet werden. +- `API/Controllers/TestItemsController.cs` als einfacher CRUD-Testcontroller (`GET/POST/PUT/DELETE`) unter `api/test-items` ergänzt. +- Dev-Stack für lokale Datenbankarbeit ergänzt: `API/Dev/docker-compose.yml` startet PostgreSQL + pgAdmin. +- `API/Program.cs` so erweitert, dass optional `appsettings.custom.json` geladen wird und bei vorhandener Datei bevorzugte lokale Overrides möglich sind. +- Neue lokale Konfigurationsdatei `API/appsettings.custom.json` aus der bisherigen `appsettings.json` angelegt und in `.gitignore` ergänzt. +- Test-Datentyp und Test-API wieder entfernt: `API/Models/Test/TestItem.cs` und `API/Controllers/TestItemsController.cs` gelöscht, `ApplicationDbContext` bereinigt. +- Neue Migration `RemoveTestItems` erstellt (`API/Migrations/20260418153650_RemoveTestItems.cs`), die Tabelle `test_items` entfernt und den Snapshot aktualisiert. diff --git a/dotnet-tools.json b/dotnet-tools.json new file mode 100644 index 0000000..3a299d9 --- /dev/null +++ b/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "10.0.6", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file