feat: add pocketbase import audit persistence

This commit is contained in:
xiong
2026-07-26 06:05:24 +08:00
parent a2b10fb05b
commit 2451c4b7a8
7 changed files with 15843 additions and 1 deletions

View File

@@ -0,0 +1,65 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Tiku.Domain.Import;
using Tiku.Domain.Tenancy;
namespace Tiku.Infrastructure.Persistence.Configurations;
internal sealed class PocketBaseImportRunConfiguration : IEntityTypeConfiguration<PocketBaseImportRun>
{
public void Configure(EntityTypeBuilder<PocketBaseImportRun> builder)
{
builder.ConfigureEntity("pb_import_runs");
builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id });
builder.Property(entity => entity.SourceName).HasMaxLength(500);
builder.Property(entity => entity.SourceKind).HasSnakeCaseEnum();
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.Property(entity => entity.Stats).IsJson("{}");
builder.Property(entity => entity.StartedAt).HasDefaultValueSql("now()");
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.StartedAt });
builder.HasOne<Tenant>().WithMany().HasForeignKey(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade);
}
}
internal sealed class PocketBaseRawRecordConfiguration : IEntityTypeConfiguration<PocketBaseRawRecord>
{
public void Configure(EntityTypeBuilder<PocketBaseRawRecord> builder)
{
builder.ToTable("pb_raw_records");
builder.HasKey(entity => new { entity.RunId, entity.CollectionName, entity.LegacyId });
builder.Property(entity => entity.CollectionName).HasMaxLength(100);
builder.Property(entity => entity.LegacyId).HasMaxLength(100);
builder.Property(entity => entity.Record).IsJson("{}");
builder.Property(entity => entity.Errors).IsJson("[]");
builder.Property(entity => entity.ImportedAt).HasDefaultValueSql("now()");
builder.HasIndex(entity => new { entity.TenantId, entity.CollectionName, entity.LegacyId });
builder.HasOne<PocketBaseImportRun>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.RunId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<Tenant>().WithMany().HasForeignKey(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade);
}
}
internal sealed class PocketBaseImportIssueConfiguration : IEntityTypeConfiguration<PocketBaseImportIssue>
{
public void Configure(EntityTypeBuilder<PocketBaseImportIssue> builder)
{
builder.ConfigureEntity("pb_import_issues");
builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id });
builder.Property(entity => entity.CollectionName).HasMaxLength(100);
builder.Property(entity => entity.LegacyId).HasMaxLength(100);
builder.Property(entity => entity.Severity).HasSnakeCaseEnum();
builder.Property(entity => entity.IssueCode).HasMaxLength(100);
builder.Property(entity => entity.FieldPath).HasMaxLength(500);
builder.Property(entity => entity.RawValueSample).HasMaxLength(2000);
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
builder.HasIndex(entity => new { entity.RunId, entity.Severity, entity.CollectionName });
builder.HasIndex(entity => new { entity.TenantId, entity.CollectionName, entity.LegacyId });
builder.HasOne<PocketBaseImportRun>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.RunId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<Tenant>().WithMany().HasForeignKey(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade);
}
}

View File

@@ -0,0 +1,148 @@
using System;
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Tiku.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddPocketBaseImportAuditPersistence : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "pb_import_runs",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
source_name = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
source_kind = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
stats = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
started_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
finished_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_pb_import_runs", x => x.id);
table.UniqueConstraint("ak_pb_import_runs_tenant_id_id", x => new { x.tenant_id, x.id });
table.ForeignKey(
name: "fk_pb_import_runs_tenants_tenant_id",
column: x => x.tenant_id,
principalTable: "tenants",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "pb_import_issues",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
run_id = table.Column<Guid>(type: "uuid", nullable: true),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
collection_name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
legacy_id = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
severity = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
issue_code = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
message = table.Column<string>(type: "text", nullable: false),
field_path = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
raw_value_sample = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
},
constraints: table =>
{
table.PrimaryKey("pk_pb_import_issues", x => x.id);
table.UniqueConstraint("ak_pb_import_issues_tenant_id_id", x => new { x.tenant_id, x.id });
table.ForeignKey(
name: "fk_pb_import_issues_pb_import_runs_tenant_id_run_id",
columns: x => new { x.tenant_id, x.run_id },
principalTable: "pb_import_runs",
principalColumns: new[] { "tenant_id", "id" },
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_pb_import_issues_tenants_tenant_id",
column: x => x.tenant_id,
principalTable: "tenants",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "pb_raw_records",
columns: table => new
{
run_id = table.Column<Guid>(type: "uuid", nullable: false),
collection_name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
legacy_id = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
record = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
normalized = table.Column<bool>(type: "boolean", nullable: false),
errors = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"),
imported_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
},
constraints: table =>
{
table.PrimaryKey("pk_pb_raw_records", x => new { x.run_id, x.collection_name, x.legacy_id });
table.ForeignKey(
name: "fk_pb_raw_records_pb_import_runs_tenant_id_run_id",
columns: x => new { x.tenant_id, x.run_id },
principalTable: "pb_import_runs",
principalColumns: new[] { "tenant_id", "id" },
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_pb_raw_records_tenants_tenant_id",
column: x => x.tenant_id,
principalTable: "tenants",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_pb_import_issues_run_id_severity_collection_name",
table: "pb_import_issues",
columns: new[] { "run_id", "severity", "collection_name" });
migrationBuilder.CreateIndex(
name: "ix_pb_import_issues_tenant_id_collection_name_legacy_id",
table: "pb_import_issues",
columns: new[] { "tenant_id", "collection_name", "legacy_id" });
migrationBuilder.CreateIndex(
name: "ix_pb_import_issues_tenant_id_run_id",
table: "pb_import_issues",
columns: new[] { "tenant_id", "run_id" });
migrationBuilder.CreateIndex(
name: "ix_pb_import_runs_tenant_id_status_started_at",
table: "pb_import_runs",
columns: new[] { "tenant_id", "status", "started_at" });
migrationBuilder.CreateIndex(
name: "ix_pb_raw_records_tenant_id_collection_name_legacy_id",
table: "pb_raw_records",
columns: new[] { "tenant_id", "collection_name", "legacy_id" });
migrationBuilder.CreateIndex(
name: "ix_pb_raw_records_tenant_id_run_id",
table: "pb_raw_records",
columns: new[] { "tenant_id", "run_id" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "pb_import_issues");
migrationBuilder.DropTable(
name: "pb_raw_records");
migrationBuilder.DropTable(
name: "pb_import_runs");
}
}
}

View File

@@ -7262,6 +7262,196 @@ namespace Tiku.Infrastructure.Persistence.Migrations
b.ToTable("user_identities", (string)null);
});
modelBuilder.Entity("Tiku.Domain.Import.PocketBaseImportIssue", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id")
.HasDefaultValueSql("gen_random_uuid()");
b.Property<string>("CollectionName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("collection_name");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at")
.HasDefaultValueSql("now()");
b.Property<string>("FieldPath")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasColumnName("field_path");
b.Property<string>("IssueCode")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("issue_code");
b.Property<string>("LegacyId")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("legacy_id");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text")
.HasColumnName("message");
b.Property<string>("RawValueSample")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)")
.HasColumnName("raw_value_sample");
b.Property<Guid?>("RunId")
.HasColumnType("uuid")
.HasColumnName("run_id");
b.Property<string>("Severity")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("severity");
b.Property<Guid>("TenantId")
.HasColumnType("uuid")
.HasColumnName("tenant_id");
b.HasKey("Id")
.HasName("pk_pb_import_issues");
b.HasAlternateKey("TenantId", "Id")
.HasName("ak_pb_import_issues_tenant_id_id");
b.HasIndex("TenantId", "RunId")
.HasDatabaseName("ix_pb_import_issues_tenant_id_run_id");
b.HasIndex("RunId", "Severity", "CollectionName")
.HasDatabaseName("ix_pb_import_issues_run_id_severity_collection_name");
b.HasIndex("TenantId", "CollectionName", "LegacyId")
.HasDatabaseName("ix_pb_import_issues_tenant_id_collection_name_legacy_id");
b.ToTable("pb_import_issues", (string)null);
});
modelBuilder.Entity("Tiku.Domain.Import.PocketBaseImportRun", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id")
.HasDefaultValueSql("gen_random_uuid()");
b.Property<DateTimeOffset?>("FinishedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("finished_at");
b.Property<string>("SourceKind")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("source_kind");
b.Property<string>("SourceName")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasColumnName("source_name");
b.Property<DateTimeOffset>("StartedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasColumnName("started_at")
.HasDefaultValueSql("now()");
b.Property<JsonElement>("Stats")
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("stats")
.HasDefaultValueSql("'{}'::jsonb");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("status");
b.Property<Guid>("TenantId")
.HasColumnType("uuid")
.HasColumnName("tenant_id");
b.HasKey("Id")
.HasName("pk_pb_import_runs");
b.HasAlternateKey("TenantId", "Id")
.HasName("ak_pb_import_runs_tenant_id_id");
b.HasIndex("TenantId", "Status", "StartedAt")
.HasDatabaseName("ix_pb_import_runs_tenant_id_status_started_at");
b.ToTable("pb_import_runs", (string)null);
});
modelBuilder.Entity("Tiku.Domain.Import.PocketBaseRawRecord", b =>
{
b.Property<Guid>("RunId")
.HasColumnType("uuid")
.HasColumnName("run_id");
b.Property<string>("CollectionName")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("collection_name");
b.Property<string>("LegacyId")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("legacy_id");
b.Property<JsonElement>("Errors")
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("errors")
.HasDefaultValueSql("'[]'::jsonb");
b.Property<DateTimeOffset>("ImportedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasColumnName("imported_at")
.HasDefaultValueSql("now()");
b.Property<bool>("Normalized")
.HasColumnType("boolean")
.HasColumnName("normalized");
b.Property<JsonElement>("Record")
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("record")
.HasDefaultValueSql("'{}'::jsonb");
b.Property<Guid>("TenantId")
.HasColumnType("uuid")
.HasColumnName("tenant_id");
b.HasKey("RunId", "CollectionName", "LegacyId")
.HasName("pk_pb_raw_records");
b.HasIndex("TenantId", "RunId")
.HasDatabaseName("ix_pb_raw_records_tenant_id_run_id");
b.HasIndex("TenantId", "CollectionName", "LegacyId")
.HasDatabaseName("ix_pb_raw_records_tenant_id_collection_name_legacy_id");
b.ToTable("pb_raw_records", (string)null);
});
modelBuilder.Entity("Tiku.Domain.Learning.AnswerRecord", b =>
{
b.Property<Guid>("Id")
@@ -13955,6 +14145,51 @@ namespace Tiku.Infrastructure.Persistence.Migrations
.HasConstraintName("fk_user_identities_users_user_id");
});
modelBuilder.Entity("Tiku.Domain.Import.PocketBaseImportIssue", b =>
{
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_pb_import_issues_tenants_tenant_id");
b.HasOne("Tiku.Domain.Import.PocketBaseImportRun", null)
.WithMany()
.HasForeignKey("TenantId", "RunId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_pb_import_issues_pb_import_runs_tenant_id_run_id");
});
modelBuilder.Entity("Tiku.Domain.Import.PocketBaseImportRun", b =>
{
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_pb_import_runs_tenants_tenant_id");
});
modelBuilder.Entity("Tiku.Domain.Import.PocketBaseRawRecord", b =>
{
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_pb_raw_records_tenants_tenant_id");
b.HasOne("Tiku.Domain.Import.PocketBaseImportRun", null)
.WithMany()
.HasForeignKey("TenantId", "RunId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_pb_raw_records_pb_import_runs_tenant_id_run_id");
});
modelBuilder.Entity("Tiku.Domain.Learning.AnswerRecord", b =>
{
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)

View File

@@ -5,6 +5,7 @@ using Tiku.Domain.Common;
using Tiku.Domain.Content;
using Tiku.Domain.Growth;
using Tiku.Domain.Identity;
using Tiku.Domain.Import;
using Tiku.Domain.Learning;
using Tiku.Domain.Operations;
using Tiku.Domain.Platform;
@@ -136,6 +137,9 @@ public sealed class TikuDbContext(DbContextOptions<TikuDbContext> options) : DbC
public DbSet<PlatformAuditAlert> PlatformAuditAlerts => Set<PlatformAuditAlert>();
public DbSet<PlatformDunningNotificationChannel> PlatformDunningNotificationChannels => Set<PlatformDunningNotificationChannel>();
public DbSet<PlatformDunningNotificationEvent> PlatformDunningNotificationEvents => Set<PlatformDunningNotificationEvent>();
public DbSet<PocketBaseImportRun> PocketBaseImportRuns => Set<PocketBaseImportRun>();
public DbSet<PocketBaseRawRecord> PocketBaseRawRecords => Set<PocketBaseRawRecord>();
public DbSet<PocketBaseImportIssue> PocketBaseImportIssues => Set<PocketBaseImportIssue>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{