fixes and tweaks

This commit is contained in:
moarten
2026-04-29 20:36:08 +02:00
parent b71f45e76c
commit 385119bb27
58 changed files with 1512 additions and 350 deletions
@@ -7,12 +7,16 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.6">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
</ItemGroup>
+32 -2
View File
@@ -15,22 +15,33 @@ public class LutraDbContext : DbContext, ILutraDbContext
public DbSet<Beoordeling> Beoordelingen => Set<Beoordeling>();
public DbSet<VerspakketFoto> VerspakketFotos => Set<VerspakketFoto>();
public DbSet<Verspakket> Verspaketten => Set<Verspakket>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Beoordeling>()
.ToTable("Beoordelingen");
// Global soft-delete filter: exclude logically deleted entities from all queries.
modelBuilder.Entity<Beoordeling>().HasQueryFilter(b => !b.IsDeleted);
modelBuilder.Entity<VerspakketFoto>().HasQueryFilter(f => !f.IsDeleted);
modelBuilder.Entity<Supermarkt>().HasQueryFilter(s => !s.IsDeleted);
modelBuilder.Entity<Verspakket>(b =>
{
b.HasQueryFilter(v => !v.IsDeleted);
b.HasMany(v => v.Beoordelingen)
.WithOne()
.HasForeignKey(beo => beo.VerspakketId)
.IsRequired();
b.HasMany(v => v.Fotos)
.WithOne()
.HasForeignKey(foto => foto.VerspakketId)
.IsRequired();
b.ToTable(t =>
{
t.HasCheckConstraint("CK_Verspaketten_AantalPersonen", "\"AantalPersonen\" BETWEEN 1 AND 10");
@@ -39,8 +50,27 @@ public class LutraDbContext : DbContext, ILutraDbContext
});
}
/// <summary>
/// Populates audit fields on tracked entities before persisting.
/// </summary>
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
var now = DateTime.UtcNow;
foreach (var entry in ChangeTracker.Entries<BaseEntity>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = now;
entry.Entity.ModifiedAt = now;
break;
case EntityState.Modified:
entry.Entity.ModifiedAt = now;
break;
}
}
return base.SaveChangesAsync(cancellationToken);
}
}
@@ -1,20 +1,58 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
namespace Lutra.Infrastructure.Sql;
/// <summary>
/// Design-time factory used by EF Core tools (dotnet ef migrations) so that
/// no startup project or live database connection is required when running migrations.
/// Reads ConnectionStrings:LutraDb from the API project's appsettings.json.
/// </summary>
internal sealed class LutraDbContextFactory : IDesignTimeDbContextFactory<LutraDbContext>
{
private const string ConnectionStringName = "LutraDb";
private const string ConnectionStringEnvironmentVariableName = "ConnectionStrings__LutraDb";
public LutraDbContext CreateDbContext(string[] args)
{
var apiProjectPath = GetApiProjectPath();
var configuration = new ConfigurationBuilder()
.SetBasePath(apiProjectPath)
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile("appsettings.Development.json", optional: true)
.AddEnvironmentVariables()
.Build();
var connectionString = configuration.GetConnectionString(ConnectionStringName);
if (string.IsNullOrWhiteSpace(connectionString) || connectionString.Contains("<set-locally>", StringComparison.Ordinal))
{
throw new InvalidOperationException($"ConnectionStrings:{ConnectionStringName} is not configured. Set it in appsettings.Development.json or via {ConnectionStringEnvironmentVariableName}.");
}
var options = new DbContextOptionsBuilder<LutraDbContext>()
.UseNpgsql("Host=localhost;Database=lutra;Username=postgres;Password=postgres")
.UseNpgsql(connectionString)
.Options;
return new LutraDbContext(options);
}
private static string GetApiProjectPath()
{
var currentDirectory = new DirectoryInfo(Directory.GetCurrentDirectory());
while (currentDirectory is not null)
{
var apiProjectPath = Path.Combine(currentDirectory.FullName, "Lutra.API");
if (Directory.Exists(apiProjectPath))
{
return apiProjectPath;
}
currentDirectory = currentDirectory.Parent;
}
throw new InvalidOperationException("Could not locate the Lutra.API project directory.");
}
}
@@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Lutra.Infrastructure.Sql.Migrations
{
/// <inheritdoc />
public partial class AddVerspakketFotos : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "VerspakketFotos",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
ModifiedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Data = table.Column<byte[]>(type: "bytea", nullable: false),
VerspakketId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_VerspakketFotos", x => x.Id);
table.ForeignKey(
name: "FK_VerspakketFotos_Verspaketten_VerspakketId",
column: x => x.VerspakketId,
principalTable: "Verspaketten",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_VerspakketFotos_VerspakketId",
table: "VerspakketFotos",
column: "VerspakketId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "VerspakketFotos");
}
}
}
@@ -0,0 +1,201 @@
// <auto-generated />
using System;
using Lutra.Infrastructure.Sql;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Lutra.Infrastructure.Sql.Migrations
{
[DbContext(typeof(LutraDbContext))]
[Migration("20260425121000_AddIsMainImageToVerspakketFoto")]
partial class AddIsMainImageToVerspakketFoto
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Aanbevolen")
.HasColumnType("boolean");
b.Property<int>("CijferBereiden")
.HasColumnType("integer");
b.Property<int>("CijferSmaak")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ModifiedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Tekst")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<Guid>("VerspakketId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("VerspakketId");
b.ToTable("Beoordelingen", (string)null);
});
modelBuilder.Entity("Lutra.Domain.Entities.Supermarkt", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ModifiedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Naam")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Supermarkten");
});
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AantalPersonen")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ModifiedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Naam")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<int?>("PrijsInCenten")
.HasColumnType("integer");
b.Property<Guid>("SupermarktId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("SupermarktId");
b.ToTable("Verspaketten", t =>
{
t.HasCheckConstraint("CK_Verspaketten_AantalPersonen", "\"AantalPersonen\" BETWEEN 1 AND 10");
t.HasCheckConstraint("CK_Verspaketten_PrijsInCenten", "\"PrijsInCenten\" IS NULL OR \"PrijsInCenten\" >= 0");
});
});
modelBuilder.Entity("Lutra.Domain.Entities.VerspakketFoto", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<byte[]>("Data")
.IsRequired()
.HasColumnType("bytea");
b.Property<bool>("IsMainImage")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ModifiedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("VerspakketId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("VerspakketId");
b.ToTable("VerspakketFotos");
});
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
{
b.HasOne("Lutra.Domain.Entities.Verspakket", null)
.WithMany("Beoordelingen")
.HasForeignKey("VerspakketId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
{
b.HasOne("Lutra.Domain.Entities.Supermarkt", "Supermarkt")
.WithMany()
.HasForeignKey("SupermarktId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Supermarkt");
});
modelBuilder.Entity("Lutra.Domain.Entities.VerspakketFoto", b =>
{
b.HasOne("Lutra.Domain.Entities.Verspakket", null)
.WithMany("Fotos")
.HasForeignKey("VerspakketId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
{
b.Navigation("Beoordelingen");
b.Navigation("Fotos");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Lutra.Infrastructure.Sql.Migrations
{
/// <inheritdoc />
public partial class AddIsMainImageToVerspakketFoto : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsMainImage",
table: "VerspakketFotos",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsMainImage",
table: "VerspakketFotos");
}
}
}
@@ -0,0 +1,201 @@
// <auto-generated />
using System;
using Lutra.Infrastructure.Sql;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Lutra.Infrastructure.Sql.Migrations
{
[DbContext(typeof(LutraDbContext))]
[Migration("20260425194247_ReplaceVerspakketFotosWithSingleMainImage")]
partial class ReplaceVerspakketFotosWithSingleMainImage
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Aanbevolen")
.HasColumnType("boolean");
b.Property<int>("CijferBereiden")
.HasColumnType("integer");
b.Property<int>("CijferSmaak")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ModifiedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Tekst")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<Guid>("VerspakketId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("VerspakketId");
b.ToTable("Beoordelingen", (string)null);
});
modelBuilder.Entity("Lutra.Domain.Entities.Supermarkt", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ModifiedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Naam")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Supermarkten");
});
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AantalPersonen")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ModifiedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Naam")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<int?>("PrijsInCenten")
.HasColumnType("integer");
b.Property<Guid>("SupermarktId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("SupermarktId");
b.ToTable("Verspaketten", t =>
{
t.HasCheckConstraint("CK_Verspaketten_AantalPersonen", "\"AantalPersonen\" BETWEEN 1 AND 10");
t.HasCheckConstraint("CK_Verspaketten_PrijsInCenten", "\"PrijsInCenten\" IS NULL OR \"PrijsInCenten\" >= 0");
});
});
modelBuilder.Entity("Lutra.Domain.Entities.VerspakketFoto", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<byte[]>("Data")
.IsRequired()
.HasColumnType("bytea");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ModifiedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("VerspakketId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("VerspakketId")
.IsUnique();
b.ToTable("VerspakketFotos", (string)null);
});
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
{
b.HasOne("Lutra.Domain.Entities.Verspakket", null)
.WithMany("Beoordelingen")
.HasForeignKey("VerspakketId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
{
b.HasOne("Lutra.Domain.Entities.Supermarkt", "Supermarkt")
.WithMany()
.HasForeignKey("SupermarktId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Supermarkt");
});
modelBuilder.Entity("Lutra.Domain.Entities.VerspakketFoto", b =>
{
b.HasOne("Lutra.Domain.Entities.Verspakket", "Verspakket")
.WithOne("MainImage")
.HasForeignKey("Lutra.Domain.Entities.VerspakketFoto", "VerspakketId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Verspakket");
});
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
{
b.Navigation("Beoordelingen");
b.Navigation("MainImage");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Lutra.Infrastructure.Sql.Migrations
{
/// <inheritdoc />
public partial class ReplaceVerspakketFotosWithSingleMainImage : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_VerspakketFotos_VerspakketId",
table: "VerspakketFotos");
migrationBuilder.DropColumn(
name: "IsMainImage",
table: "VerspakketFotos");
migrationBuilder.CreateIndex(
name: "IX_VerspakketFotos_VerspakketId",
table: "VerspakketFotos",
column: "VerspakketId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_VerspakketFotos_VerspakketId",
table: "VerspakketFotos");
migrationBuilder.AddColumn<bool>(
name: "IsMainImage",
table: "VerspakketFotos",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.CreateIndex(
name: "IX_VerspakketFotos_VerspakketId",
table: "VerspakketFotos",
column: "VerspakketId");
}
}
}
@@ -17,7 +17,7 @@ namespace Lutra.Infrastructure.Sql.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.6")
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -126,6 +126,36 @@ namespace Lutra.Infrastructure.Sql.Migrations
});
});
modelBuilder.Entity("Lutra.Domain.Entities.VerspakketFoto", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<byte[]>("Data")
.IsRequired()
.HasColumnType("bytea");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ModifiedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("VerspakketId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("VerspakketId")
.IsUnique();
b.ToTable("VerspakketFotos", (string)null);
});
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
{
b.HasOne("Lutra.Domain.Entities.Verspakket", null)
@@ -146,9 +176,21 @@ namespace Lutra.Infrastructure.Sql.Migrations
b.Navigation("Supermarkt");
});
modelBuilder.Entity("Lutra.Domain.Entities.VerspakketFoto", b =>
{
b.HasOne("Lutra.Domain.Entities.Verspakket", "Verspakket")
.WithOne("MainImage")
.HasForeignKey("Lutra.Domain.Entities.VerspakketFoto", "VerspakketId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Verspakket");
});
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
{
b.Navigation("Beoordelingen");
b.Navigation("MainImage");
});
#pragma warning restore 612, 618
}