Added tech base model

This commit is contained in:
2026-01-06 02:38:26 +01:00
parent 0a57915447
commit 097e06b328
20 changed files with 441 additions and 33 deletions

View File

@@ -17,8 +17,6 @@ var app = builder.Build();
using (var scope = app.Services.CreateScope()) using (var scope = app.Services.CreateScope())
{ {
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
context.Database.EnsureCreated();
var migrationService = scope.ServiceProvider.GetRequiredService<MigrationService>(); var migrationService = scope.ServiceProvider.GetRequiredService<MigrationService>();
await migrationService.Migrate(); await migrationService.Migrate();
} }

View File

@@ -1,5 +1,5 @@
using dc_bot.db.Model; using dc_bot.db.Model.administration;
using dc_bot.db.Model.administration; using dc_bot.db.Model.permission;
using dc_bot.db.Model.system; using dc_bot.db.Model.system;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -8,19 +8,17 @@ namespace dc_bot.db;
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options) public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{ {
public required DbSet<ExecutedMigration> ExecutedMigrations { get; set; } public required DbSet<ExecutedMigration> ExecutedMigrations { get; set; }
public required DbSet<User> Users { get; set; } public required DbSet<User> Users { get; set; }
public required DbSet<ApiKey> ApiKeys { get; set; }
public required DbSet<Role> Roles { get; set; }
public required DbSet<Permission> Permissions { get; set; }
public required DbSet<RolePermission> RolePermissions { get; set; }
public required DbSet<ApiKeyPermission> ApiKeyPermissions { get; set; }
public required DbSet<RoleUser> RoleUsers { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.Entity<ExecutedMigration>(e =>
{
e.ToTable("_executed_migrations", "system");
e.HasKey(e => e.MigrationId);
});
modelBuilder.Entity<User>(e =>
{
e.ToTable("users", "administration");
});
} }
} }

View File

@@ -1,6 +1,6 @@
using System.Runtime.CompilerServices;
using dc_bot.db.Model.system; using dc_bot.db.Model.system;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace dc_bot.db; namespace dc_bot.db;
@@ -34,20 +34,21 @@ public class MigrationService
using var scope = _scopeFactory.CreateScope(); using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>(); var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await EnsureExecutedMigrationsTableExistsAsync(context);
var appliedMigrations = await context.ExecutedMigrations var appliedMigrations = await context.ExecutedMigrations
.Select(m => m.MigrationId) .Select(m => m.MigrationId)
.ToListAsync(); .ToListAsync();
var appliedMigrationNames = appliedMigrations var migrations = GetMigrations();
.Select(m => m.Replace(".sql", string.Empty)) var migrationsToApply = migrations
.ToHashSet(StringComparer.OrdinalIgnoreCase); .Where(m => !appliedMigrations.Contains(m.Replace(".sql", string.Empty)))
var migrations = GetMigrations()
.Where(m => !appliedMigrationNames.Contains(m))
.ToList(); .ToList();
foreach (var migration in migrations) foreach (var migration in migrationsToApply)
{ {
await using var transaction = await context.Database.BeginTransactionAsync();
try try
{ {
var migrationPath = Path.Combine(MigrationDirectory, migration); var migrationPath = Path.Combine(MigrationDirectory, migration);
@@ -57,18 +58,59 @@ public class MigrationService
var executedMigration = new ExecutedMigration var executedMigration = new ExecutedMigration
{ {
MigrationId = migration, MigrationId = migration.Replace(".sql", string.Empty),
Created = DateTime.UtcNow, Created = DateTime.UtcNow,
Updated = DateTime.UtcNow Updated = DateTime.UtcNow
}; };
context.ExecutedMigrations.Add(executedMigration); context.ExecutedMigrations.Add(executedMigration);
await context.SaveChangesAsync(); await context.SaveChangesAsync();
await transaction.CommitAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"Error applying migration {migration}: {ex.Message}"); try
{
await transaction.RollbackAsync();
}
catch
{
// ignored
}
Console.WriteLine($"Error applying migration: {migration} => {ex.Message}");
throw; throw;
} }
} }
} }
private async Task EnsureExecutedMigrationsTableExistsAsync(AppDbContext context)
{
var entityType = context.Model.FindEntityType(typeof(ExecutedMigration));
if (entityType == null)
return;
var schema = entityType.GetSchema() ?? "public";
var tableName = entityType.GetTableName() ?? entityType.GetDefaultTableName();
// Spaltennamen ermitteln (relational)
var storeIdentifier = StoreObjectIdentifier.Table(tableName, schema);
string migrationIdCol = entityType.FindProperty(nameof(ExecutedMigration.MigrationId))
.GetColumnName(storeIdentifier);
string createdCol = entityType.FindProperty(nameof(ExecutedMigration.Created))
.GetColumnName(storeIdentifier);
string updatedCol = entityType.FindProperty(nameof(ExecutedMigration.Updated))
.GetColumnName(storeIdentifier);
var createSchemaSql = $"CREATE SCHEMA IF NOT EXISTS \"{schema}\";";
var createTableSql = $@"
CREATE TABLE IF NOT EXISTS ""{schema}"".""{tableName}"" (
""{migrationIdCol}"" TEXT PRIMARY KEY,
""{createdCol}"" TIMESTAMPTZ NOT NULL,
""{updatedCol}"" TIMESTAMPTZ NOT NULL
);
";
await context.Database.ExecuteSqlRawAsync(createSchemaSql + createTableSql);
}
} }

View File

@@ -2,8 +2,9 @@ namespace dc_bot.db.Model;
public interface IDbModel public interface IDbModel
{ {
public double Id { get; set; }
public bool Deleted { get; set; } public bool Deleted { get; set; }
public Double EditorId { get; set; } public double EditorId { get; set; }
public DateTimeOffset Created { get; set; } public DateTimeOffset Created { get; set; }
public DateTimeOffset Updated { get; set; } public DateTimeOffset Updated { get; set; }
} }

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace dc_bot.db.Model.administration;
[Table("api_keys", Schema = "administration")]
public class ApiKey : IDbModel
{
public double Id { get; set; }
public required string Identifier { get; init; }
public required string Key { get; init; }
public bool Deleted { get; set; }
public double EditorId { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset Updated { get; set; }
}

View File

@@ -1,8 +1,11 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace dc_bot.db.Model.administration; namespace dc_bot.db.Model.administration;
[Table("users", Schema = "administration")]
public class User : IDbModel public class User : IDbModel
{ {
public Double Id { get; set; } public double Id { get; set; }
public required string KeycloakId { get; init; } public required string KeycloakId { get; init; }
public bool Deleted { get; set; } public bool Deleted { get; set; }

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace dc_bot.db.Model.permission;
[Table("api_key_permissions", Schema = "permission")]
public class ApiKeyPermission : IDbModel
{
public double Id { get; set; }
public double ApiKeyId { get; set; }
public double PermissionId { get; set; }
public bool Deleted { get; set; }
public double EditorId { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset Updated { get; set; }
}

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace dc_bot.db.Model.permission;
[Table("permissions", Schema = "permission")]
public class Permission : IDbModel
{
public double Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool Deleted { get; set; }
public double EditorId { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset Updated { get; set; }
}

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace dc_bot.db.Model.permission;
[Table("roles", Schema = "permission")]
public class Role : IDbModel
{
public double Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool Deleted { get; set; }
public double EditorId { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset Updated { get; set; }
}

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace dc_bot.db.Model.permission;
[Table("role_permissions", Schema = "permission")]
public class RolePermission : IDbModel
{
public double Id { get; set; }
public double RoleId { get; set; }
public double PermissionId { get; set; }
public bool Deleted { get; set; }
public double EditorId { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset Updated { get; set; }
}

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace dc_bot.db.Model.permission;
[Table("role_users", Schema = "permission")]
public class RoleUser : IDbModel
{
public double Id { get; set; }
public double UserId { get; set; }
public double PermissionId { get; set; }
public bool Deleted { get; set; }
public double EditorId { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset Updated { get; set; }
}

View File

@@ -1,7 +1,10 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace dc_bot.db.Model.system; namespace dc_bot.db.Model.system;
[Table("_executed_migrations", Schema = "system")]
[PrimaryKey(nameof(MigrationId))]
public class ExecutedMigration public class ExecutedMigration
{ {
public required string MigrationId { get; set; } public required string MigrationId { get; set; }

View File

@@ -7,3 +7,6 @@ CREATE TABLE IF NOT EXISTS system._executed_migrations
Created timestamptz NOT NULL DEFAULT NOW(), Created timestamptz NOT NULL DEFAULT NOW(),
Updated timestamptz NOT NULL DEFAULT NOW() Updated timestamptz NOT NULL DEFAULT NOW()
); );
DROP EXTENSION IF EXISTS fuzzystrmatch;
CREATE EXTENSION fuzzystrmatch SCHEMA public;

View File

@@ -6,13 +6,15 @@ CREATE TABLE IF NOT EXISTS administration.users
KeycloakId UUID NOT NULL, KeycloakId UUID NOT NULL,
-- for history -- for history
Deleted BOOLEAN NOT NULL DEFAULT FALSE, Deleted BOOLEAN NOT NULL DEFAULT FALSE,
EditorId INT NULL REFERENCES administration.users (Id), EditorId BIGINT NULL REFERENCES administration.users (Id),
Created timestamptz NOT NULL DEFAULT NOW(), Created timestamptz NOT NULL DEFAULT NOW(),
Updated timestamptz NOT NULL DEFAULT NOW(), Updated timestamptz NOT NULL DEFAULT NOW()
CONSTRAINT UC_KeycloakId UNIQUE (KeycloakId)
); );
CREATE UNIQUE INDEX IF NOT EXISTS IX_UC_KeycloakId
ON administration.users (KeycloakId)
WHERE KeycloakId != '00000000-0000-0000-0000-000000000000';
CREATE TABLE IF NOT EXISTS administration.users_history CREATE TABLE IF NOT EXISTS administration.users_history
( (
LIKE administration.users LIKE administration.users

View File

@@ -0,0 +1,105 @@
CREATE SCHEMA IF NOT EXISTS permission;
-- Permissions
CREATE TABLE permission.permissions
(
Id SERIAL PRIMARY KEY,
Name VARCHAR(255) NOT NULL,
Description TEXT NULL,
-- for history
Deleted BOOLEAN NOT NULL DEFAULT FALSE,
EditorId BIGINT NULL REFERENCES administration.users (Id),
Created timestamptz NOT NULL DEFAULT NOW(),
Updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UQ_PermissionName UNIQUE (Name)
);
CREATE TABLE permission.permissions_history
(
LIKE permission.permissions
);
CREATE TRIGGER versioning_trigger
BEFORE INSERT OR UPDATE OR DELETE
ON permission.permissions
FOR EACH ROW
EXECUTE PROCEDURE public.history_trigger_function();
-- Roles
CREATE TABLE permission.roles
(
Id SERIAL PRIMARY KEY,
Name VARCHAR(255) NOT NULL,
Description TEXT NULL,
-- for history
Deleted BOOLEAN NOT NULL DEFAULT FALSE,
EditorId BIGINT NULL REFERENCES administration.users (Id),
Created timestamptz NOT NULL DEFAULT NOW(),
Updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UQ_RoleName UNIQUE (Name)
);
CREATE TABLE permission.roles_history
(
LIKE permission.roles
);
CREATE TRIGGER versioning_trigger
BEFORE INSERT OR UPDATE OR DELETE
ON permission.roles
FOR EACH ROW
EXECUTE PROCEDURE public.history_trigger_function();
-- Role permissions
CREATE TABLE permission.role_permissions
(
Id SERIAL PRIMARY KEY,
RoleId BIGINT NOT NULL REFERENCES permission.roles (Id) ON DELETE CASCADE,
PermissionId BIGINT NOT NULL REFERENCES permission.permissions (Id) ON DELETE CASCADE,
-- for history
Deleted BOOLEAN NOT NULL DEFAULT FALSE,
EditorId BIGINT NULL REFERENCES administration.users (Id),
Created timestamptz NOT NULL DEFAULT NOW(),
Updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UQ_RolePermission UNIQUE (RoleId, PermissionId)
);
CREATE TABLE permission.role_permissions_history
(
LIKE permission.role_permissions
);
CREATE TRIGGER versioning_trigger
BEFORE INSERT OR UPDATE OR DELETE
ON permission.role_permissions
FOR EACH ROW
EXECUTE PROCEDURE public.history_trigger_function();
-- Role user
CREATE TABLE permission.role_users
(
Id SERIAL PRIMARY KEY,
RoleId BIGINT NOT NULL REFERENCES permission.roles (Id) ON DELETE CASCADE,
UserId BIGINT NOT NULL REFERENCES administration.users (Id) ON DELETE CASCADE,
-- for history
Deleted BOOLEAN NOT NULL DEFAULT FALSE,
EditorId BIGINT NULL REFERENCES administration.users (Id),
Created timestamptz NOT NULL DEFAULT NOW(),
Updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UQ_RoleUser UNIQUE (RoleId, UserId)
);
CREATE TABLE permission.role_users_history
(
LIKE permission.role_users
);
CREATE TRIGGER versioning_trigger
BEFORE INSERT OR UPDATE OR DELETE
ON permission.role_users
FOR EACH ROW
EXECUTE PROCEDURE public.history_trigger_function();

View File

@@ -0,0 +1,50 @@
CREATE TABLE IF NOT EXISTS administration.api_keys
(
Id SERIAL PRIMARY KEY,
Identifier VARCHAR(255) NOT NULL,
KeyString VARCHAR(255) NOT NULL,
-- for history
Deleted BOOLEAN NOT NULL DEFAULT FALSE,
EditorId BIGINT NULL REFERENCES administration.users (Id),
Created timestamptz NOT NULL DEFAULT NOW(),
Updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UC_Identifier_Key UNIQUE (Identifier, KeyString),
CONSTRAINT UC_Key UNIQUE (KeyString)
);
CREATE TABLE IF NOT EXISTS administration.api_keys_history
(
LIKE administration.api_keys
);
CREATE TRIGGER api_keys_history_trigger
BEFORE INSERT OR UPDATE OR DELETE
ON administration.api_keys
FOR EACH ROW
EXECUTE FUNCTION public.history_trigger_function();
CREATE TABLE permission.api_key_permissions
(
Id SERIAL PRIMARY KEY,
ApiKeyId BIGINT NOT NULL REFERENCES administration.api_keys (Id) ON DELETE CASCADE,
PermissionId BIGINT NOT NULL REFERENCES permission.permissions (Id) ON DELETE CASCADE,
-- for history
Deleted BOOLEAN NOT NULL DEFAULT FALSE,
EditorId BIGINT NULL REFERENCES administration.users (Id),
Created timestamptz NOT NULL DEFAULT NOW(),
Updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UQ_ApiKeyPermission UNIQUE (ApiKeyId, PermissionId)
);
CREATE TABLE permission.api_key_permissions_history
(
LIKE permission.api_key_permissions
);
CREATE TRIGGER versioning_trigger
BEFORE INSERT OR UPDATE OR DELETE
ON permission.api_key_permissions
FOR EACH ROW
EXECUTE PROCEDURE public.history_trigger_function();

View File

@@ -0,0 +1,26 @@
CREATE SCHEMA IF NOT EXISTS system;
CREATE TABLE IF NOT EXISTS system.settings
(
Id SERIAL PRIMARY KEY,
Key TEXT NOT NULL,
Value TEXT NOT NULL,
Type TEXT NOT NULL DEFAULT 'string' CHECK (type IN ('string', 'number', 'boolean')),
-- for history
Deleted BOOLEAN NOT NULL DEFAULT FALSE,
EditorId BIGINT NULL REFERENCES administration.users (Id),
Created timestamptz NOT NULL DEFAULT NOW(),
Updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UC_Key UNIQUE (Key)
);
CREATE TABLE system.settings_history
(
LIKE system.settings
);
CREATE TRIGGER settings_history_trigger
BEFORE INSERT OR UPDATE OR DELETE
ON system.settings
FOR EACH ROW
EXECUTE FUNCTION public.history_trigger_function();

View File

@@ -0,0 +1,25 @@
CREATE SCHEMA IF NOT EXISTS system;
CREATE TABLE IF NOT EXISTS system.feature_flags
(
Id SERIAL PRIMARY KEY,
Key TEXT NOT NULL,
Value BOOLEAN NOT NULL,
-- for history
Deleted BOOLEAN NOT NULL DEFAULT FALSE,
EditorId BIGINT NULL REFERENCES administration.users (Id),
Created timestamptz NOT NULL DEFAULT NOW(),
Updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UQ_FeatureFlagKey UNIQUE (Key)
);
CREATE TABLE system.feature_flags_history
(
LIKE system.feature_flags
);
CREATE TRIGGER feature_flags_history_trigger
BEFORE INSERT OR UPDATE OR DELETE
ON system.feature_flags
FOR EACH ROW
EXECUTE FUNCTION public.history_trigger_function();

View File

@@ -0,0 +1,26 @@
CREATE SCHEMA IF NOT EXISTS public;
CREATE TABLE IF NOT EXISTS public.user_settings
(
Id SERIAL PRIMARY KEY,
Key TEXT NOT NULL,
Value TEXT NOT NULL,
UserId BIGINT NOT NULL REFERENCES administration.users (Id) ON DELETE CASCADE,
-- for history
Deleted BOOLEAN NOT NULL DEFAULT FALSE,
EditorId BIGINT NULL REFERENCES administration.users (Id),
Created timestamptz NOT NULL DEFAULT NOW(),
Updated timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT UQ_UserSetting UNIQUE (UserId, Key)
);
CREATE TABLE public.user_settings_history
(
LIKE public.user_settings
);
CREATE TRIGGER user_settings_history_trigger
BEFORE INSERT OR UPDATE OR DELETE
ON public.user_settings
FOR EACH ROW
EXECUTE FUNCTION public.history_trigger_function();

View File

@@ -10,6 +10,36 @@
<None Update="Scripts\**"> <None Update="Scripts\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="Scripts\2026-01-06-01-15-api_keys.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Scripts\2026-01-06-01-16-permissions.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Scripts\2026-01-06-01-19-user-settings.sql">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scripts\2026-01-05-21-15-users.sql">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scripts\2026-01-05-21-05-inital-migration.sql">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scripts\2026-01-05-21-10-inital-trigger.sql">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scripts\2026-01-06-01-15-permissions.sql">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scripts\2026-01-06-01-16-api-keys.sql">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scripts\2026-01-06-01-17-settings.sql">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scripts\2026-01-06-01-18-feature-flags.sql">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>