401 lines
17 KiB
C#
401 lines
17 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
using Tiku.Domain.Content;
|
|
using Tiku.Domain.Identity;
|
|
using Tiku.Domain.Learning;
|
|
using Tiku.Domain.Tenancy;
|
|
|
|
namespace Tiku.Infrastructure.Persistence.Configurations;
|
|
|
|
internal sealed class PracticeSessionConfiguration : IEntityTypeConfiguration<PracticeSession>
|
|
{
|
|
public void Configure(EntityTypeBuilder<PracticeSession> builder)
|
|
{
|
|
builder.ConfigureTenantEntity("practice_sessions");
|
|
builder.HasAlternateKey(entity => new { entity.TenantId, entity.UserId, entity.Id });
|
|
builder.Property(entity => entity.Mode).HasMaxLength(50);
|
|
builder.Property(entity => entity.ResourceType).HasSnakeCaseEnum();
|
|
builder.Property(entity => entity.StartedAt).HasDefaultValueSql("now()");
|
|
builder.Property(entity => entity.TotalScore).HasPrecision(8, 2);
|
|
builder.Property(entity => entity.Status).HasSnakeCaseEnum().HasDefaultValue(PracticeSessionStatus.Active);
|
|
builder.Property(entity => entity.Version).IsConcurrencyToken().HasDefaultValue(1L);
|
|
builder.Property(entity => entity.AccessMode)
|
|
.HasSnakeCaseEnum()
|
|
.HasDefaultValue(PracticeAccessMode.Free);
|
|
builder.Property(entity => entity.AccessSnapshot).IsJson("{}");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.StrongRevocationVersion });
|
|
builder.Property(entity => entity.Metadata).IsJson("{}");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.StartedAt });
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.Status, entity.StartedAt });
|
|
|
|
builder.ToTable(table =>
|
|
{
|
|
table.HasCheckConstraint(
|
|
"ck_practice_sessions_consumed_free_quota",
|
|
"consumed_free_quota >= 0");
|
|
});
|
|
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.UserId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
}
|
|
}
|
|
|
|
internal sealed class PracticeSessionQuestionConfiguration : IEntityTypeConfiguration<PracticeSessionQuestion>
|
|
{
|
|
public void Configure(EntityTypeBuilder<PracticeSessionQuestion> builder)
|
|
{
|
|
builder.ConfigureTenantEntity("practice_session_questions");
|
|
builder.HasAlternateKey(entity => new { entity.TenantId, entity.PracticeSessionId, entity.Id });
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.PracticeSessionId, entity.Position }).IsUnique();
|
|
builder.HasIndex(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.PracticeSessionId,
|
|
entity.ContentReleaseQuestionId
|
|
}).IsUnique();
|
|
builder.HasIndex(entity => new { entity.QuestionOwnerTenantId, entity.ContentReleaseQuestionId })
|
|
.HasDatabaseName("ix_psq_release_question");
|
|
builder.HasIndex(entity => new { entity.QuestionOwnerTenantId, entity.QuestionAssetId })
|
|
.HasDatabaseName("ix_psq_question_asset");
|
|
builder.HasIndex(entity => new
|
|
{
|
|
entity.QuestionOwnerTenantId,
|
|
entity.QuestionAssetId,
|
|
entity.QuestionRevisionId
|
|
})
|
|
.HasDatabaseName("ix_psq_question_revision");
|
|
builder.HasIndex(entity => new { entity.QuestionOwnerTenantId, entity.QuestionPlacementId })
|
|
.HasDatabaseName("ix_psq_question_placement");
|
|
builder.HasIndex(entity => new { entity.QuestionOwnerTenantId, entity.AssessmentPolicyVersionId })
|
|
.HasDatabaseName("ix_psq_assessment_policy");
|
|
builder.Property(entity => entity.Score).HasPrecision(8, 2);
|
|
builder.Property(entity => entity.QuestionType).HasMaxLength(50).HasDefaultValue("choice");
|
|
builder.Property(entity => entity.TypeLabelSnapshot).HasMaxLength(100);
|
|
builder.Property(entity => entity.TagsSnapshot).IsJson("[]");
|
|
builder.Property(entity => entity.GradingRulesSnapshot).IsJson("{}");
|
|
builder.Property(entity => entity.SnapshotVersion).HasDefaultValue(1);
|
|
|
|
builder.HasOne<PracticeSession>().WithMany()
|
|
.HasForeignKey(entity => new { entity.TenantId, entity.PracticeSessionId })
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
builder.HasOne<ContentReleaseQuestion>().WithMany()
|
|
.HasForeignKey(entity => new
|
|
{
|
|
TenantId = entity.QuestionOwnerTenantId,
|
|
Id = entity.ContentReleaseQuestionId
|
|
})
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
builder.HasOne<QuestionAsset>().WithMany()
|
|
.HasForeignKey(entity => new
|
|
{
|
|
TenantId = entity.QuestionOwnerTenantId,
|
|
Id = entity.QuestionAssetId
|
|
})
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
builder.HasOne<QuestionRevision>().WithMany()
|
|
.HasForeignKey(entity => new
|
|
{
|
|
TenantId = entity.QuestionOwnerTenantId,
|
|
entity.QuestionAssetId,
|
|
Id = entity.QuestionRevisionId
|
|
})
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionAssetId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
builder.HasOne<QuestionPlacement>().WithMany()
|
|
.HasForeignKey(entity => new
|
|
{
|
|
TenantId = entity.QuestionOwnerTenantId,
|
|
Id = entity.QuestionPlacementId
|
|
})
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
builder.HasOne<AssessmentPolicyVersion>().WithMany()
|
|
.HasForeignKey(entity => new
|
|
{
|
|
TenantId = entity.QuestionOwnerTenantId,
|
|
Id = entity.AssessmentPolicyVersionId
|
|
})
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
}
|
|
}
|
|
|
|
internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration<AnswerRecord>
|
|
{
|
|
public void Configure(EntityTypeBuilder<AnswerRecord> builder)
|
|
{
|
|
builder.ConfigureTenantEntity("answer_records");
|
|
builder.Property(entity => entity.LegacyId).HasMaxLength(64);
|
|
builder.Property(entity => entity.LegacyQuestionId).HasMaxLength(64);
|
|
builder.Property(entity => entity.LegacyCategoryId).HasMaxLength(64);
|
|
builder.Property(entity => entity.SelectedOptions).IsJson("[]");
|
|
builder.Property(entity => entity.GradingStatus).HasSnakeCaseEnum()
|
|
.HasDefaultValue(AnswerGradingStatus.PendingReview);
|
|
builder.Property(entity => entity.AwardedScore).HasPrecision(8, 2);
|
|
builder.Property(entity => entity.IdempotencyKey).HasMaxLength(200);
|
|
builder.Property(entity => entity.RequestHash).HasMaxLength(64);
|
|
builder.Property(entity => entity.Revision).HasDefaultValue(1);
|
|
builder.Property(entity => entity.AnsweredAt).HasDefaultValueSql("now()");
|
|
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
|
|
builder.HasIndex(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.UserId,
|
|
entity.PracticeSessionId
|
|
});
|
|
builder.HasIndex(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.UserId,
|
|
entity.PracticeSessionId,
|
|
entity.ClientSequence
|
|
}).IsUnique();
|
|
builder.HasIndex(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.UserId,
|
|
entity.PracticeSessionId,
|
|
entity.IdempotencyKey
|
|
}).IsUnique().HasDatabaseName("ux_answer_records_idempotency");
|
|
builder.HasIndex(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.UserId,
|
|
entity.PracticeSessionId,
|
|
entity.SessionQuestionId,
|
|
entity.Revision
|
|
}).IsUnique().HasDatabaseName("ux_answer_records_session_question_revision");
|
|
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.UserId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
builder.HasOne<PracticeSession>().WithMany()
|
|
.HasForeignKey(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.UserId,
|
|
entity.PracticeSessionId
|
|
})
|
|
.HasPrincipalKey(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.UserId,
|
|
entity.Id
|
|
})
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
builder.HasOne<PracticeSessionQuestion>().WithMany()
|
|
.HasForeignKey(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.PracticeSessionId,
|
|
Id = entity.SessionQuestionId
|
|
})
|
|
.HasPrincipalKey(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.PracticeSessionId,
|
|
entity.Id
|
|
})
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
}
|
|
}
|
|
|
|
internal sealed class CurrentAnswerConfiguration : IEntityTypeConfiguration<CurrentAnswer>
|
|
{
|
|
public void Configure(EntityTypeBuilder<CurrentAnswer> builder)
|
|
{
|
|
builder.ConfigureTenantEntity("current_answers");
|
|
builder.Property(entity => entity.Version).IsConcurrencyToken().HasDefaultValue(1L);
|
|
builder.Property(entity => entity.UpdatedAt).HasDefaultValueSql("now()");
|
|
builder.HasIndex(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.UserId,
|
|
entity.PracticeSessionId,
|
|
entity.SessionQuestionId
|
|
}).IsUnique().HasDatabaseName("ux_current_answers_session_question");
|
|
|
|
builder.HasOne<AnswerRecord>().WithMany()
|
|
.HasForeignKey(entity => new { entity.TenantId, Id = entity.AnswerRecordId })
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
builder.HasOne<PracticeSessionQuestion>().WithMany()
|
|
.HasForeignKey(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.PracticeSessionId,
|
|
Id = entity.SessionQuestionId
|
|
})
|
|
.HasPrincipalKey(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.PracticeSessionId,
|
|
entity.Id
|
|
})
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
}
|
|
}
|
|
|
|
internal sealed class LearningOperationIdempotencyConfiguration : IEntityTypeConfiguration<LearningOperationIdempotency>
|
|
{
|
|
public void Configure(EntityTypeBuilder<LearningOperationIdempotency> builder)
|
|
{
|
|
builder.ConfigureEntity("learning_operation_idempotencies");
|
|
builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id });
|
|
builder.Property(entity => entity.OperationType).HasMaxLength(50);
|
|
builder.Property(entity => entity.IdempotencyKey).HasMaxLength(200);
|
|
builder.Property(entity => entity.RequestHash).HasMaxLength(64);
|
|
builder.Property(entity => entity.ResponseSnapshot).IsJson("{}");
|
|
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
|
builder.HasIndex(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.UserId,
|
|
entity.PracticeSessionId,
|
|
entity.OperationType,
|
|
entity.IdempotencyKey
|
|
}).IsUnique();
|
|
|
|
builder.HasOne<Tenant>().WithMany()
|
|
.HasForeignKey(entity => entity.TenantId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.UserId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
builder.HasOne<PracticeSession>().WithMany()
|
|
.HasForeignKey(entity => new { entity.TenantId, entity.UserId, Id = entity.PracticeSessionId })
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.UserId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
}
|
|
}
|
|
|
|
internal sealed class FavoriteQuestionConfiguration : IEntityTypeConfiguration<FavoriteQuestion>
|
|
{
|
|
public void Configure(EntityTypeBuilder<FavoriteQuestion> builder)
|
|
{
|
|
builder.ToTable("favorite_questions");
|
|
builder.HasKey(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.UserId,
|
|
entity.QuestionAssetOwnerTenantId,
|
|
entity.QuestionAssetId
|
|
});
|
|
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.CreatedAt });
|
|
|
|
builder.HasOne<Tenant>().WithMany()
|
|
.HasForeignKey(entity => entity.TenantId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.UserId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
ConfigureQuestionAssetEvidence(builder);
|
|
}
|
|
|
|
private static void ConfigureQuestionAssetEvidence(EntityTypeBuilder<FavoriteQuestion> builder)
|
|
{
|
|
builder.HasOne<QuestionAsset>().WithMany()
|
|
.HasForeignKey(entity => new
|
|
{
|
|
TenantId = entity.QuestionAssetOwnerTenantId,
|
|
Id = entity.QuestionAssetId
|
|
})
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
builder.HasOne<QuestionRevision>().WithMany()
|
|
.HasForeignKey(entity => new
|
|
{
|
|
TenantId = entity.QuestionAssetOwnerTenantId,
|
|
entity.QuestionAssetId,
|
|
Id = entity.LastQuestionRevisionId
|
|
})
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionAssetId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
builder.HasOne<QuestionPlacement>().WithMany()
|
|
.HasForeignKey(entity => new
|
|
{
|
|
TenantId = entity.QuestionAssetOwnerTenantId,
|
|
Id = entity.LastQuestionPlacementId
|
|
})
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
}
|
|
}
|
|
|
|
internal sealed class WrongQuestionConfiguration : IEntityTypeConfiguration<WrongQuestion>
|
|
{
|
|
public void Configure(EntityTypeBuilder<WrongQuestion> builder)
|
|
{
|
|
builder.ToTable("wrong_questions");
|
|
builder.HasKey(entity => new
|
|
{
|
|
entity.TenantId,
|
|
entity.UserId,
|
|
entity.QuestionAssetOwnerTenantId,
|
|
entity.QuestionAssetId
|
|
});
|
|
builder.Property(entity => entity.WrongCount).HasDefaultValue(1);
|
|
builder.Property(entity => entity.LastWrongAt).HasDefaultValueSql("now()");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.LastWrongAt });
|
|
|
|
builder.HasOne<Tenant>().WithMany()
|
|
.HasForeignKey(entity => entity.TenantId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.UserId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
builder.HasOne<QuestionAsset>().WithMany()
|
|
.HasForeignKey(entity => new
|
|
{
|
|
TenantId = entity.QuestionAssetOwnerTenantId,
|
|
Id = entity.QuestionAssetId
|
|
})
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
builder.HasOne<QuestionRevision>().WithMany()
|
|
.HasForeignKey(entity => new
|
|
{
|
|
TenantId = entity.QuestionAssetOwnerTenantId,
|
|
entity.QuestionAssetId,
|
|
Id = entity.LastQuestionRevisionId
|
|
})
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionAssetId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
builder.HasOne<QuestionPlacement>().WithMany()
|
|
.HasForeignKey(entity => new
|
|
{
|
|
TenantId = entity.QuestionAssetOwnerTenantId,
|
|
Id = entity.LastQuestionPlacementId
|
|
})
|
|
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
}
|
|
}
|
|
|
|
internal sealed class RecentPracticeConfiguration : IEntityTypeConfiguration<RecentPractice>
|
|
{
|
|
public void Configure(EntityTypeBuilder<RecentPractice> builder)
|
|
{
|
|
builder.ConfigureTenantEntity("recent_practices");
|
|
builder.ConfigureTimestamps();
|
|
builder.Property(entity => entity.LegacyId).HasMaxLength(64);
|
|
builder.Property(entity => entity.PracticeType).HasMaxLength(50);
|
|
builder.Property(entity => entity.TargetLegacyId).HasMaxLength(64);
|
|
builder.Property(entity => entity.TargetName).HasMaxLength(300);
|
|
builder.Property(entity => entity.Color).HasMaxLength(50);
|
|
builder.Property(entity => entity.Metadata).IsJson("{}");
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
|
|
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.LastAccessAt });
|
|
|
|
builder.HasOne<User>().WithMany()
|
|
.HasForeignKey(entity => entity.UserId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
}
|
|
}
|