diff --git a/Directory.Packages.props b/Directory.Packages.props index 3d2772e..04a58a5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,9 +11,18 @@ all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/Tiku.Api/Program.cs b/Tiku.Api/Program.cs index ee9d65d..8000192 100644 --- a/Tiku.Api/Program.cs +++ b/Tiku.Api/Program.cs @@ -21,7 +21,7 @@ var summaries = new[] app.MapGet("/weatherforecast", () => { - var forecast = Enumerable.Range(1, 5).Select(index => + var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), diff --git a/Tiku.Application/Class1.cs b/Tiku.Application/Class1.cs deleted file mode 100644 index d5a5d22..0000000 --- a/Tiku.Application/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Tiku.Application; - -public class Class1 -{ - -} diff --git a/Tiku.DbMigrator/DesignTimeTikuDbContextFactory.cs b/Tiku.DbMigrator/DesignTimeTikuDbContextFactory.cs new file mode 100644 index 0000000..d3ad663 --- /dev/null +++ b/Tiku.DbMigrator/DesignTimeTikuDbContextFactory.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.DbMigrator; + +public sealed class DesignTimeTikuDbContextFactory : IDesignTimeDbContextFactory +{ + public TikuDbContext CreateDbContext(string[] args) + { + var connectionString = + Environment.GetEnvironmentVariable("DATABASE_URL") ?? + "Host=localhost;Database=tiku;Username=postgres"; + + var options = new DbContextOptionsBuilder() + .UseNpgsql(connectionString, npgsql => + npgsql.MigrationsAssembly(typeof(TikuDbContext).Assembly.FullName)) + .Options; + + return new TikuDbContext(options); + } +} diff --git a/Tiku.DbMigrator/Program.cs b/Tiku.DbMigrator/Program.cs index 3751555..346f94c 100644 --- a/Tiku.DbMigrator/Program.cs +++ b/Tiku.DbMigrator/Program.cs @@ -1,2 +1,20 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Tiku.Infrastructure; +using Tiku.Infrastructure.Persistence; + +var builder = Host.CreateApplicationBuilder(args); +var connectionString = + builder.Configuration.GetConnectionString("Database") ?? + Environment.GetEnvironmentVariable("DATABASE_URL") ?? + throw new InvalidOperationException( + "Database connection is required. Configure ConnectionStrings:Database or DATABASE_URL."); + +builder.Services.AddInfrastructure(connectionString); + +using var host = builder.Build(); +await using var scope = host.Services.CreateAsyncScope(); +var dbContext = scope.ServiceProvider.GetRequiredService(); +await dbContext.Database.MigrateAsync(); diff --git a/Tiku.DbMigrator/Tiku.DbMigrator.csproj b/Tiku.DbMigrator/Tiku.DbMigrator.csproj index a868c9e..7463c78 100644 --- a/Tiku.DbMigrator/Tiku.DbMigrator.csproj +++ b/Tiku.DbMigrator/Tiku.DbMigrator.csproj @@ -4,6 +4,14 @@ + + + all + all + + + + Exe net10.0 diff --git a/Tiku.Domain/Catalog/CatalogEntities.cs b/Tiku.Domain/Catalog/CatalogEntities.cs new file mode 100644 index 0000000..0c6375b --- /dev/null +++ b/Tiku.Domain/Catalog/CatalogEntities.cs @@ -0,0 +1,126 @@ +using System.Text.Json; +using Tiku.Domain.Common; + +namespace Tiku.Domain.Catalog; + +public sealed class Region : AuditableTenantEntity +{ + public string? LegacyId { get; set; } + public string Name { get; set; } = string.Empty; + public string? Code { get; set; } + public string? ShortName { get; set; } + public string? FullName { get; set; } + public string? Icon { get; set; } + public string? Pinyin { get; set; } + public int SortOrder { get; set; } + public bool IsHot { get; set; } + public bool IsActive { get; set; } = true; + public JsonElement Config { get; set; } = JsonDefaults.Object(); +} + +public sealed class RegionModule : AuditableTenantEntity +{ + public Guid? RegionId { get; set; } + public string? LegacyId { get; set; } + public string Name { get; set; } = string.Empty; + public string? Type { get; set; } + public string? Icon { get; set; } + public string? Color { get; set; } + public string? TextColor { get; set; } + public string? Description { get; set; } + public string? Route { get; set; } + public int SortOrder { get; set; } + public bool IsPrimarySchoolModule { get; set; } + public bool IsActive { get; set; } = true; +} + +public sealed class ModuleNode : AuditableTenantEntity +{ + public Guid? RegionId { get; set; } + public Guid? ModuleId { get; set; } + public Guid? ParentId { get; set; } + public string? LegacyId { get; set; } + public string? LegacyParentId { get; set; } + public string? LegacyModuleId { get; set; } + public ModuleNodeType Type { get; set; } = ModuleNodeType.Custom; + public string Name { get; set; } = string.Empty; + public string? Path { get; set; } + public int SortOrder { get; set; } + public bool IsActive { get; set; } = true; + public JsonElement Metadata { get; set; } = JsonDefaults.Object(); +} + +public enum ModuleNodeType +{ + Category, + Subject, + Chapter, + Paper, + School, + Major, + Custom +} + +public sealed class School : AuditableTenantEntity +{ + public Guid? RegionId { get; set; } + public Guid? ModuleId { get; set; } + public string? LegacyId { get; set; } + public string Name { get; set; } = string.Empty; + public string? ProfessionalExamDate { get; set; } + public JsonElement Metadata { get; set; } = JsonDefaults.Object(); +} + +public sealed class Major : AuditableTenantEntity +{ + public Guid? RegionId { get; set; } + public Guid? SchoolId { get; set; } + public string? LegacyId { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public string? StudyTips { get; set; } + public int SortOrder { get; set; } + public bool IsActive { get; set; } = true; +} + +public sealed class Subject : AuditableTenantEntity +{ + public Guid? RegionId { get; set; } + public Guid? ModuleId { get; set; } + public Guid? SchoolId { get; set; } + public Guid? MajorId { get; set; } + public Guid? NodeId { get; set; } + public string? LegacyId { get; set; } + public string Name { get; set; } = string.Empty; + public SubjectType? Type { get; set; } + public JsonElement MajorLegacyIds { get; set; } = JsonDefaults.Array(); + public string? Icon { get; set; } + public string? Description { get; set; } + public JsonElement Stats { get; set; } = JsonDefaults.Object(); + public int SortOrder { get; set; } + public bool IsActive { get; set; } = true; +} + +public enum SubjectType +{ + Cultural, + Professional +} + +public sealed class Category : AuditableTenantEntity +{ + public Guid? SubjectId { get; set; } + public Guid? NodeId { get; set; } + public string? LegacyId { get; set; } + public string Name { get; set; } = string.Empty; + public CategoryType? CategoryType { get; set; } + public int SortOrder { get; set; } + public int? SvipQuestionLimit { get; set; } + public bool IsActive { get; set; } = true; +} + +public enum CategoryType +{ + Chapter, + Paper +} diff --git a/Tiku.Domain/Class1.cs b/Tiku.Domain/Class1.cs deleted file mode 100644 index 4a5d7aa..0000000 --- a/Tiku.Domain/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Tiku.Domain; - -public class Class1 -{ - -} diff --git a/Tiku.Domain/Common/Entity.cs b/Tiku.Domain/Common/Entity.cs new file mode 100644 index 0000000..dac48d5 --- /dev/null +++ b/Tiku.Domain/Common/Entity.cs @@ -0,0 +1,46 @@ +using System.Text.Json; + +namespace Tiku.Domain.Common; + +public abstract class Entity +{ + public Guid Id { get; set; } = Guid.NewGuid(); +} + +public interface IHasTimestamps +{ + DateTimeOffset CreatedAt { get; set; } + DateTimeOffset UpdatedAt { get; set; } +} + +public abstract class AuditableEntity : Entity, IHasTimestamps +{ + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow; +} + +public abstract class TenantEntity : Entity +{ + public Guid TenantId { get; set; } +} + +public abstract class AuditableTenantEntity : TenantEntity, IHasTimestamps +{ + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow; +} + +public static class JsonDefaults +{ + public static JsonElement Object() + { + using var document = JsonDocument.Parse("{}"); + return document.RootElement.Clone(); + } + + public static JsonElement Array() + { + using var document = JsonDocument.Parse("[]"); + return document.RootElement.Clone(); + } +} diff --git a/Tiku.Domain/Identity/StudentProfile.cs b/Tiku.Domain/Identity/StudentProfile.cs new file mode 100644 index 0000000..ef2803b --- /dev/null +++ b/Tiku.Domain/Identity/StudentProfile.cs @@ -0,0 +1,20 @@ +using System.Text.Json; +using Tiku.Domain.Common; + +namespace Tiku.Domain.Identity; + +public sealed class StudentProfile : AuditableTenantEntity +{ + public Guid UserId { get; set; } + public string? LegacyUserId { get; set; } + public Guid? RegionId { get; set; } + public Guid? SelectedSchoolId { get; set; } + public Guid? SelectedMajorId { get; set; } + public int QuestionsAnsweredToday { get; set; } + public int MasteredWordsCount { get; set; } + public DateOnly? LastCheckInDate { get; set; } + public JsonElement Stats { get; set; } = JsonDefaults.Object(); + public JsonElement Progress { get; set; } = JsonDefaults.Object(); + public JsonElement ModuleSelections { get; set; } = JsonDefaults.Object(); + public JsonElement RecentActivities { get; set; } = JsonDefaults.Array(); +} diff --git a/Tiku.Domain/Identity/User.cs b/Tiku.Domain/Identity/User.cs new file mode 100644 index 0000000..69c98e0 --- /dev/null +++ b/Tiku.Domain/Identity/User.cs @@ -0,0 +1,20 @@ +using System.Text.Json; +using Tiku.Domain.Common; + +namespace Tiku.Domain.Identity; + +public sealed class User : AuditableEntity +{ + public string? LegacyId { get; set; } + public string? Username { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + public string? Name { get; set; } + public string? AvatarUrl { get; set; } + public string PrimaryRole { get; set; } = "student"; + public int Score { get; set; } + public DateTimeOffset? LastSeenAt { get; set; } + public string? LegacyPasswordHash { get; set; } + public bool PasswordMigrationRequired { get; set; } + public JsonElement RawProfile { get; set; } = JsonDefaults.Object(); +} diff --git a/Tiku.Domain/Identity/UserIdentity.cs b/Tiku.Domain/Identity/UserIdentity.cs new file mode 100644 index 0000000..e268546 --- /dev/null +++ b/Tiku.Domain/Identity/UserIdentity.cs @@ -0,0 +1,16 @@ +using System.Text.Json; +using Tiku.Domain.Common; + +namespace Tiku.Domain.Identity; + +public sealed class UserIdentity : AuditableEntity +{ + public Guid UserId { get; set; } + public string Provider { get; set; } = string.Empty; + public string ProviderSubject { get; set; } = string.Empty; + public string? UnionId { get; set; } + public string? OpenId { get; set; } + public string? Phone { get; set; } + public string? Email { get; set; } + public JsonElement SecretPayload { get; set; } = JsonDefaults.Object(); +} diff --git a/Tiku.Domain/Learning/LearningEntities.cs b/Tiku.Domain/Learning/LearningEntities.cs new file mode 100644 index 0000000..e273261 --- /dev/null +++ b/Tiku.Domain/Learning/LearningEntities.cs @@ -0,0 +1,64 @@ +using System.Text.Json; +using Tiku.Domain.Common; + +namespace Tiku.Domain.Learning; + +public sealed class PracticeSession : TenantEntity +{ + public Guid UserId { get; set; } + public string Mode { get; set; } = "chapter"; + public string? TargetType { get; set; } + public Guid? TargetId { get; set; } + public DateTimeOffset StartedAt { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? FinishedAt { get; set; } + public JsonElement Metadata { get; set; } = JsonDefaults.Object(); +} + +public sealed class AnswerRecord : TenantEntity +{ + public Guid UserId { get; set; } + public Guid? QuestionId { get; set; } + public Guid? QuestionVersionId { get; set; } + public Guid? PracticeSessionId { get; set; } + public string? LegacyId { get; set; } + public string? LegacyQuestionId { get; set; } + public string? LegacyCategoryId { get; set; } + public JsonElement SelectedOptions { get; set; } = JsonDefaults.Array(); + public string? AnswerText { get; set; } + public bool? IsCorrect { get; set; } + public DateTimeOffset AnsweredAt { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; +} + +public sealed class FavoriteQuestion +{ + public Guid TenantId { get; set; } + public Guid UserId { get; set; } + public Guid QuestionId { get; set; } + public string Source { get; set; } = "imported"; + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; +} + +public sealed class WrongQuestion +{ + public Guid TenantId { get; set; } + public Guid UserId { get; set; } + public Guid QuestionId { get; set; } + public int WrongCount { get; set; } = 1; + public DateTimeOffset LastWrongAt { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? ResolvedAt { get; set; } +} + +public sealed class RecentPractice : AuditableTenantEntity +{ + public Guid? UserId { get; set; } + public string? LegacyId { get; set; } + public string? PracticeType { get; set; } + public string? TargetLegacyId { get; set; } + public string? TargetName { get; set; } + public int Progress { get; set; } + public string? Color { get; set; } + public DateTimeOffset? LastAccessAt { get; set; } + public DateTimeOffset? LastPracticeAt { get; set; } + public JsonElement Metadata { get; set; } = JsonDefaults.Object(); +} diff --git a/Tiku.Domain/QuestionBanks/QuestionEntities.cs b/Tiku.Domain/QuestionBanks/QuestionEntities.cs new file mode 100644 index 0000000..183fcab --- /dev/null +++ b/Tiku.Domain/QuestionBanks/QuestionEntities.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using Tiku.Domain.Common; + +namespace Tiku.Domain.QuestionBanks; + +public sealed class QuestionBank : AuditableTenantEntity +{ + public Guid? RegionId { get; set; } + public string Name { get; set; } = string.Empty; + public QuestionBankScope SourceScope { get; set; } = QuestionBankScope.Tenant; + public QuestionBankStatus Status { get; set; } = QuestionBankStatus.Active; + public JsonElement Metadata { get; set; } = JsonDefaults.Object(); +} + +public enum QuestionBankScope +{ + Platform, + Tenant +} + +public enum QuestionBankStatus +{ + Active, + Archived +} + +public sealed class Question : AuditableTenantEntity +{ + public Guid? QuestionBankId { get; set; } + public Guid? SubjectId { get; set; } + public Guid? CategoryId { get; set; } + public Guid? NodeId { get; set; } + public string? LegacyId { get; set; } + public string? LegacySubjectId { get; set; } + public string? LegacyCategoryId { get; set; } + public string? LegacyNodeId { get; set; } + public string Type { get; set; } = "choice"; + public string? TypeLabel { get; set; } + public int? Difficulty { get; set; } + public JsonElement Tags { get; set; } = JsonDefaults.Array(); + public string? MediaUrl { get; set; } + public bool HasVideoExplanation { get; set; } + public QuestionStatus Status { get; set; } = QuestionStatus.Published; + public Guid? CurrentVersionId { get; set; } +} + +public enum QuestionStatus +{ + Draft, + Published, + Archived +} + +public sealed class QuestionVersion : Entity +{ + public Guid TenantId { get; set; } + public Guid QuestionId { get; set; } + public int VersionNo { get; set; } = 1; + public string? Content { get; set; } + public JsonElement Options { get; set; } = JsonDefaults.Array(); + public int? CorrectOptionIndex { get; set; } + public JsonElement CorrectOptionIndices { get; set; } = JsonDefaults.Array(); + public string? AnswerText { get; set; } + public string? Explanation { get; set; } + public JsonElement SubQuestions { get; set; } = JsonDefaults.Array(); + public string? CodeLang { get; set; } + public string? CodeTemplate { get; set; } + public string? SourceHash { get; set; } + public Guid? CreatedBy { get; set; } + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; +} diff --git a/Tiku.Domain/Tenancy/Tenant.cs b/Tiku.Domain/Tenancy/Tenant.cs new file mode 100644 index 0000000..3454f2c --- /dev/null +++ b/Tiku.Domain/Tenancy/Tenant.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using Tiku.Domain.Common; + +namespace Tiku.Domain.Tenancy; + +public sealed class Tenant : AuditableEntity +{ + public string Slug { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string? LegalName { get; set; } + public TenantStatus Status { get; set; } = TenantStatus.Active; + public TenantMode Mode { get; set; } = TenantMode.Saas; + public BillingStatus BillingStatus { get; set; } = BillingStatus.Trial; + public Guid? OwnerUserId { get; set; } + public string? LegacyId { get; set; } + public JsonElement Metadata { get; set; } = JsonDefaults.Object(); +} + +public enum TenantStatus +{ + Draft, + Active, + Suspended, + Archived +} + +public enum TenantMode +{ + PlatformOwned, + Saas, + Dedicated +} + +public enum BillingStatus +{ + Trial, + Active, + PastDue, + Cancelled +} diff --git a/Tiku.Domain/Tenancy/TenantMembership.cs b/Tiku.Domain/Tenancy/TenantMembership.cs new file mode 100644 index 0000000..224fbeb --- /dev/null +++ b/Tiku.Domain/Tenancy/TenantMembership.cs @@ -0,0 +1,32 @@ +using System.Text.Json; +using Tiku.Domain.Common; + +namespace Tiku.Domain.Tenancy; + +public sealed class TenantMembership : AuditableTenantEntity +{ + public Guid UserId { get; set; } + public TenantRole Role { get; set; } = TenantRole.Student; + public MembershipStatus Status { get; set; } = MembershipStatus.Active; + public JsonElement Permissions { get; set; } = JsonDefaults.Object(); + public string? LegacyRole { get; set; } +} + +public enum TenantRole +{ + PlatformAdmin, + TenantOwner, + TenantAdmin, + TenantOperator, + Teacher, + Sales, + Agent, + Student +} + +public enum MembershipStatus +{ + Active, + Invited, + Disabled +} diff --git a/Tiku.Infrastructure/Class1.cs b/Tiku.Infrastructure/Class1.cs deleted file mode 100644 index 631e294..0000000 --- a/Tiku.Infrastructure/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Tiku.Infrastructure; - -public class Class1 -{ - -} diff --git a/Tiku.Infrastructure/DependencyInjection.cs b/Tiku.Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000..3026ec7 --- /dev/null +++ b/Tiku.Infrastructure/DependencyInjection.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Npgsql; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddInfrastructure( + this IServiceCollection services, + string connectionString) + { + ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); + + services.AddSingleton(_ => NpgsqlDataSource.Create(connectionString)); + services.AddDbContextPool((serviceProvider, options) => + { + var dataSource = serviceProvider.GetRequiredService(); + options.UseNpgsql(dataSource, npgsql => + npgsql.MigrationsAssembly(typeof(TikuDbContext).Assembly.FullName)); + }); + + return services; + } +} diff --git a/Tiku.Infrastructure/Persistence/Configurations/CatalogConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/CatalogConfigurations.cs new file mode 100644 index 0000000..357ef0e --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Configurations/CatalogConfigurations.cs @@ -0,0 +1,187 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tiku.Domain.Catalog; + +namespace Tiku.Infrastructure.Persistence.Configurations; + +internal sealed class RegionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("regions"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.LegacyId).HasMaxLength(64); + builder.Property(entity => entity.Name).HasMaxLength(200); + builder.Property(entity => entity.Code).HasMaxLength(50); + builder.Property(entity => entity.ShortName).HasMaxLength(100); + builder.Property(entity => entity.FullName).HasMaxLength(300); + builder.Property(entity => entity.Icon).HasMaxLength(2048); + builder.Property(entity => entity.Pinyin).HasMaxLength(255); + builder.Property(entity => entity.Config).IsJson("{}"); + builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique(); + } +} + +internal sealed class RegionModuleConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("region_modules"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.LegacyId).HasMaxLength(64); + builder.Property(entity => entity.Name).HasMaxLength(200); + builder.Property(entity => entity.Type).HasMaxLength(50); + builder.Property(entity => entity.Icon).HasMaxLength(2048); + builder.Property(entity => entity.Color).HasMaxLength(50); + builder.Property(entity => entity.TextColor).HasMaxLength(50); + builder.Property(entity => entity.Route).HasMaxLength(500); + builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique(); + + builder.HasOne() + .WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.RegionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class ModuleNodeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("module_nodes"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.LegacyId).HasMaxLength(64); + builder.Property(entity => entity.LegacyParentId).HasMaxLength(64); + builder.Property(entity => entity.LegacyModuleId).HasMaxLength(64); + builder.Property(entity => entity.Type).HasSnakeCaseEnum(); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.Property(entity => entity.Path).HasMaxLength(1000); + builder.Property(entity => entity.Metadata).IsJson("{}"); + builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique(); + + builder.HasOne() + .WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.RegionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne() + .WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.ModuleId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne() + .WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.ParentId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class SchoolConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("schools"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.LegacyId).HasMaxLength(64); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.Property(entity => entity.ProfessionalExamDate).HasMaxLength(255); + builder.Property(entity => entity.Metadata).IsJson("{}"); + builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique(); + + builder.HasOne() + .WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.RegionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne() + .WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.ModuleId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class MajorConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("majors"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.LegacyId).HasMaxLength(64); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique(); + + builder.HasOne() + .WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.RegionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne() + .WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.SchoolId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class SubjectConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("subjects"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.LegacyId).HasMaxLength(64); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.Property(entity => entity.Type).HasNullableSnakeCaseEnum(); + builder.Property(entity => entity.MajorLegacyIds).IsJson("[]"); + builder.Property(entity => entity.Icon).HasMaxLength(2048); + builder.Property(entity => entity.Stats).IsJson("{}"); + builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique(); + + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.RegionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.ModuleId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.SchoolId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.MajorId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.NodeId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } + +} + +internal sealed class CategoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("categories"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.LegacyId).HasMaxLength(64); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.Property(entity => entity.CategoryType).HasNullableSnakeCaseEnum(); + builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique(); + + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.SubjectId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.NodeId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/Tiku.Infrastructure/Persistence/Configurations/ConfigurationSupport.cs b/Tiku.Infrastructure/Persistence/Configurations/ConfigurationSupport.cs new file mode 100644 index 0000000..edf9655 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Configurations/ConfigurationSupport.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tiku.Domain.Common; +using Tiku.Domain.Tenancy; + +namespace Tiku.Infrastructure.Persistence.Configurations; + +internal static class ConfigurationSupport +{ + public static void ConfigureEntity( + this EntityTypeBuilder builder, + string tableName) + where TEntity : Entity + { + builder.ToTable(tableName); + builder.HasKey(entity => entity.Id); + builder.Property(entity => entity.Id).HasDefaultValueSql("gen_random_uuid()"); + } + + public static void ConfigureTenantEntity( + this EntityTypeBuilder builder, + string tableName) + where TEntity : TenantEntity + { + builder.ConfigureEntity(tableName); + builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id }); + builder.HasOne() + .WithMany() + .HasForeignKey(entity => entity.TenantId) + .OnDelete(DeleteBehavior.Cascade); + } + + public static void ConfigureTimestamps(this EntityTypeBuilder builder) + where TEntity : class, IHasTimestamps + { + builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); + builder.Property(entity => entity.UpdatedAt).HasDefaultValueSql("now()"); + } + + public static PropertyBuilder IsJson( + this PropertyBuilder property, + string defaultJson) + { + return property + .HasColumnType("jsonb") + .HasDefaultValueSql($"'{defaultJson}'::jsonb"); + } + + public static PropertyBuilder HasSnakeCaseEnum( + this PropertyBuilder property, + int maxLength = 32) + where TEnum : struct, Enum + { + return property + .HasConversion(new SnakeCaseEnumConverter()) + .HasMaxLength(maxLength); + } + + public static PropertyBuilder HasNullableSnakeCaseEnum( + this PropertyBuilder property, + int maxLength = 32) + where TEnum : struct, Enum + { + return property + .HasConversion( + value => value.HasValue + ? ModelBuilderExtensions.ToSnakeCase(value.Value.ToString()) + : null, + value => string.IsNullOrWhiteSpace(value) + ? null + : Enum.Parse(value.Replace("_", string.Empty), true)) + .HasMaxLength(maxLength); + } +} diff --git a/Tiku.Infrastructure/Persistence/Configurations/IdentityConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/IdentityConfigurations.cs new file mode 100644 index 0000000..fc64c1f --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Configurations/IdentityConfigurations.cs @@ -0,0 +1,90 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tiku.Domain.Catalog; +using Tiku.Domain.Identity; + +namespace Tiku.Infrastructure.Persistence.Configurations; + +internal sealed class UserConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureEntity("users"); + builder.ConfigureTimestamps(); + + builder.Property(entity => entity.LegacyId).HasMaxLength(64); + builder.Property(entity => entity.Username).HasMaxLength(100); + builder.Property(entity => entity.Email).HasColumnType("citext").HasMaxLength(320); + builder.Property(entity => entity.Phone).HasMaxLength(32); + builder.Property(entity => entity.Name).HasMaxLength(200); + builder.Property(entity => entity.AvatarUrl).HasMaxLength(2048); + builder.Property(entity => entity.PrimaryRole).HasMaxLength(50); + builder.Property(entity => entity.LegacyPasswordHash).HasMaxLength(512); + builder.Property(entity => entity.RawProfile).IsJson("{}"); + + builder.HasIndex(entity => entity.LegacyId).IsUnique(); + builder.HasIndex(entity => entity.Username).IsUnique(); + builder.HasIndex(entity => entity.Email).IsUnique(); + builder.HasIndex(entity => entity.Phone).IsUnique(); + } +} + +internal sealed class UserIdentityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureEntity("user_identities"); + builder.ConfigureTimestamps(); + + builder.Property(entity => entity.Provider).HasMaxLength(50); + builder.Property(entity => entity.ProviderSubject).HasMaxLength(255); + builder.Property(entity => entity.UnionId).HasMaxLength(255); + builder.Property(entity => entity.OpenId).HasMaxLength(255); + builder.Property(entity => entity.Phone).HasMaxLength(32); + builder.Property(entity => entity.Email).HasColumnType("citext").HasMaxLength(320); + builder.Property(entity => entity.SecretPayload).IsJson("{}"); + + builder.HasIndex(entity => new { entity.Provider, entity.ProviderSubject }).IsUnique(); + builder.HasOne() + .WithMany() + .HasForeignKey(entity => entity.UserId) + .OnDelete(DeleteBehavior.Cascade); + } +} + +internal sealed class StudentProfileConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("student_profiles"); + builder.ConfigureTimestamps(); + + builder.Property(entity => entity.LegacyUserId).HasMaxLength(64); + builder.Property(entity => entity.Stats).IsJson("{}"); + builder.Property(entity => entity.Progress).IsJson("{}"); + builder.Property(entity => entity.ModuleSelections).IsJson("{}"); + builder.Property(entity => entity.RecentActivities).IsJson("[]"); + + builder.HasIndex(entity => new { entity.TenantId, entity.UserId }).IsUnique(); + + builder.HasOne() + .WithMany() + .HasForeignKey(entity => entity.UserId) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne() + .WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.RegionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne() + .WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.SelectedSchoolId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne() + .WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.SelectedMajorId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs new file mode 100644 index 0000000..ff85448 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs @@ -0,0 +1,159 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tiku.Domain.Identity; +using Tiku.Domain.Learning; +using Tiku.Domain.QuestionBanks; +using Tiku.Domain.Tenancy; + +namespace Tiku.Infrastructure.Persistence.Configurations; + +internal sealed class PracticeSessionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder 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.TargetType).HasMaxLength(50); + builder.Property(entity => entity.StartedAt).HasDefaultValueSql("now()"); + builder.Property(entity => entity.Metadata).IsJson("{}"); + builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.StartedAt }); + + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.UserId) + .OnDelete(DeleteBehavior.Cascade); + } +} + +internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder 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.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.QuestionId }); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.QuestionId, + entity.QuestionVersionId + }); + builder.HasIndex(entity => new + { + entity.TenantId, + entity.UserId, + entity.PracticeSessionId + }); + + builder.ToTable(table => table.HasCheckConstraint( + "ck_answer_records_version_requires_question", + "question_version_id is null or question_id is not null")); + + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.UserId) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.QuestionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new + { + entity.TenantId, + entity.QuestionId, + entity.QuestionVersionId + }) + .HasPrincipalKey(entity => new + { + entity.TenantId, + entity.QuestionId, + entity.Id + }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new + { + entity.TenantId, + entity.UserId, + entity.PracticeSessionId + }) + .HasPrincipalKey(entity => new + { + entity.TenantId, + entity.UserId, + entity.Id + }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class FavoriteQuestionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("favorite_questions"); + builder.HasKey(entity => new { entity.TenantId, entity.UserId, entity.QuestionId }); + builder.Property(entity => entity.Source).HasMaxLength(50); + builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); + + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.TenantId) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.UserId) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.QuestionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + } +} + +internal sealed class WrongQuestionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("wrong_questions"); + builder.HasKey(entity => new { entity.TenantId, entity.UserId, entity.QuestionId }); + builder.Property(entity => entity.WrongCount).HasDefaultValue(1); + builder.Property(entity => entity.LastWrongAt).HasDefaultValueSql("now()"); + + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.TenantId) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.UserId) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.QuestionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + } +} + +internal sealed class RecentPracticeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder 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().WithMany() + .HasForeignKey(entity => entity.UserId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/Tiku.Infrastructure/Persistence/Configurations/QuestionConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/QuestionConfigurations.cs new file mode 100644 index 0000000..6f2a629 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Configurations/QuestionConfigurations.cs @@ -0,0 +1,92 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tiku.Domain.Catalog; +using Tiku.Domain.Identity; +using Tiku.Domain.QuestionBanks; + +namespace Tiku.Infrastructure.Persistence.Configurations; + +internal sealed class QuestionBankConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("question_banks"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.Name).HasMaxLength(300); + builder.Property(entity => entity.SourceScope).HasSnakeCaseEnum(); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.Metadata).IsJson("{}"); + + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.RegionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class QuestionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("questions"); + builder.ConfigureTimestamps(); + builder.Property(entity => entity.LegacyId).HasMaxLength(64); + builder.Property(entity => entity.LegacySubjectId).HasMaxLength(64); + builder.Property(entity => entity.LegacyCategoryId).HasMaxLength(64); + builder.Property(entity => entity.LegacyNodeId).HasMaxLength(64); + builder.Property(entity => entity.Type).HasMaxLength(50); + builder.Property(entity => entity.TypeLabel).HasMaxLength(100); + builder.Property(entity => entity.Tags).IsJson("[]"); + builder.Property(entity => entity.MediaUrl).HasMaxLength(2048); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique(); + + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.QuestionBankId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.SubjectId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.CategoryId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.NodeId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.Id, entity.CurrentVersionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionId, entity.Id }) + .OnDelete(DeleteBehavior.Restrict); + } +} + +internal sealed class QuestionVersionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureEntity("question_versions"); + builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id }); + builder.HasAlternateKey(entity => new { entity.TenantId, entity.QuestionId, entity.Id }); + + builder.Property(entity => entity.Options).IsJson("[]"); + builder.Property(entity => entity.CorrectOptionIndices).IsJson("[]"); + builder.Property(entity => entity.SubQuestions).IsJson("[]"); + builder.Property(entity => entity.CodeLang).HasMaxLength(50); + builder.Property(entity => entity.SourceHash).HasMaxLength(128); + builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()"); + + builder.HasIndex(entity => new { entity.QuestionId, entity.VersionNo }).IsUnique(); + builder.HasOne().WithMany() + .HasForeignKey(entity => new { entity.TenantId, entity.QuestionId }) + .HasPrincipalKey(entity => new { entity.TenantId, entity.Id }) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne().WithMany() + .HasForeignKey(entity => entity.CreatedBy) + .OnDelete(DeleteBehavior.SetNull); + } +} diff --git a/Tiku.Infrastructure/Persistence/Configurations/TenancyConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/TenancyConfigurations.cs new file mode 100644 index 0000000..d7cb689 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Configurations/TenancyConfigurations.cs @@ -0,0 +1,53 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tiku.Domain.Identity; +using Tiku.Domain.Tenancy; + +namespace Tiku.Infrastructure.Persistence.Configurations; + +internal sealed class TenantConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureEntity("tenants"); + builder.ConfigureTimestamps(); + + builder.Property(entity => entity.Slug).HasColumnType("citext").HasMaxLength(100); + builder.Property(entity => entity.Name).HasMaxLength(200); + builder.Property(entity => entity.LegalName).HasMaxLength(300); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.Mode).HasSnakeCaseEnum(); + builder.Property(entity => entity.BillingStatus).HasSnakeCaseEnum(); + builder.Property(entity => entity.LegacyId).HasMaxLength(64); + builder.Property(entity => entity.Metadata).IsJson("{}"); + + builder.HasIndex(entity => entity.Slug).IsUnique(); + builder.HasIndex(entity => entity.LegacyId).IsUnique(); + + builder.HasOne() + .WithMany() + .HasForeignKey(entity => entity.OwnerUserId) + .OnDelete(DeleteBehavior.SetNull); + } +} + +internal sealed class TenantMembershipConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureTenantEntity("tenant_memberships"); + builder.ConfigureTimestamps(); + + builder.Property(entity => entity.Role).HasSnakeCaseEnum(); + builder.Property(entity => entity.Status).HasSnakeCaseEnum(); + builder.Property(entity => entity.Permissions).IsJson("{}"); + builder.Property(entity => entity.LegacyRole).HasMaxLength(50); + + builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.Role }).IsUnique(); + + builder.HasOne() + .WithMany() + .HasForeignKey(entity => entity.UserId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/Tiku.Infrastructure/Persistence/Migrations/20260725205004_InitialCore.Designer.cs b/Tiku.Infrastructure/Persistence/Migrations/20260725205004_InitialCore.Designer.cs new file mode 100644 index 0000000..3083ab8 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/20260725205004_InitialCore.Designer.cs @@ -0,0 +1,2176 @@ +// +using System; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Tiku.Infrastructure.Persistence; + +#nullable disable + +namespace Tiku.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(TikuDbContext))] + [Migration("20260725205004_InitialCore")] + partial class InitialCore + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Tiku.Domain.Catalog.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CategoryType") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("category_type"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("SubjectId") + .HasColumnType("uuid") + .HasColumnName("subject_id"); + + b.Property("SvipQuestionLimit") + .HasColumnType("integer") + .HasColumnName("svip_question_limit"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_categories"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_categories_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_categories_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "NodeId") + .HasDatabaseName("ix_categories_tenant_id_node_id"); + + b.HasIndex("TenantId", "SubjectId") + .HasDatabaseName("ix_categories_tenant_id_subject_id"); + + b.ToTable("categories", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Major", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SchoolId") + .HasColumnType("uuid") + .HasColumnName("school_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("StudyTips") + .HasColumnType("text") + .HasColumnName("study_tips"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_majors"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_majors_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_majors_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_majors_tenant_id_region_id"); + + b.HasIndex("TenantId", "SchoolId") + .HasDatabaseName("ix_majors_tenant_id_school_id"); + + b.ToTable("majors", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.ModuleNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyModuleId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_module_id"); + + b.Property("LegacyParentId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_parent_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("Path") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("path"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_module_nodes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_module_nodes_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_module_nodes_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "ModuleId") + .HasDatabaseName("ix_module_nodes_tenant_id_module_id"); + + b.HasIndex("TenantId", "ParentId") + .HasDatabaseName("ix_module_nodes_tenant_id_parent_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_module_nodes_tenant_id_region_id"); + + b.ToTable("module_nodes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("Config") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("config") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FullName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("full_name"); + + b.Property("Icon") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("icon"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsHot") + .HasColumnType("boolean") + .HasColumnName("is_hot"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Pinyin") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("pinyin"); + + b.Property("ShortName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("short_name"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_regions_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_regions_tenant_id_legacy_id"); + + b.ToTable("regions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.RegionModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Color") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("color"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Icon") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("icon"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsPrimarySchoolModule") + .HasColumnType("boolean") + .HasColumnName("is_primary_school_module"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("Route") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("route"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TextColor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("text_color"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_region_modules"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_region_modules_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_region_modules_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_region_modules_tenant_id_region_id"); + + b.ToTable("region_modules", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.School", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("ProfessionalExamDate") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("professional_exam_date"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_schools"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_schools_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_schools_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "ModuleId") + .HasDatabaseName("ix_schools_tenant_id_module_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_schools_tenant_id_region_id"); + + b.ToTable("schools", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Subject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Icon") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("icon"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("MajorId") + .HasColumnType("uuid") + .HasColumnName("major_id"); + + b.Property("MajorLegacyIds") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("major_legacy_ids") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SchoolId") + .HasColumnType("uuid") + .HasColumnName("school_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("Stats") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("stats") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Type") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_subjects"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_subjects_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_subjects_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "MajorId") + .HasDatabaseName("ix_subjects_tenant_id_major_id"); + + b.HasIndex("TenantId", "ModuleId") + .HasDatabaseName("ix_subjects_tenant_id_module_id"); + + b.HasIndex("TenantId", "NodeId") + .HasDatabaseName("ix_subjects_tenant_id_node_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_subjects_tenant_id_region_id"); + + b.HasIndex("TenantId", "SchoolId") + .HasDatabaseName("ix_subjects_tenant_id_school_id"); + + b.ToTable("subjects", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.StudentProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LastCheckInDate") + .HasColumnType("date") + .HasColumnName("last_check_in_date"); + + b.Property("LegacyUserId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_user_id"); + + b.Property("MasteredWordsCount") + .HasColumnType("integer") + .HasColumnName("mastered_words_count"); + + b.Property("ModuleSelections") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("module_selections") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Progress") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("progress") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("QuestionsAnsweredToday") + .HasColumnType("integer") + .HasColumnName("questions_answered_today"); + + b.Property("RecentActivities") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("recent_activities") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SelectedMajorId") + .HasColumnType("uuid") + .HasColumnName("selected_major_id"); + + b.Property("SelectedSchoolId") + .HasColumnType("uuid") + .HasColumnName("selected_school_id"); + + b.Property("Stats") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("stats") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_student_profiles"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_profiles_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_profiles_user_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_student_profiles_tenant_id_region_id"); + + b.HasIndex("TenantId", "SelectedMajorId") + .HasDatabaseName("ix_student_profiles_tenant_id_selected_major_id"); + + b.HasIndex("TenantId", "SelectedSchoolId") + .HasDatabaseName("ix_student_profiles_tenant_id_selected_school_id"); + + b.HasIndex("TenantId", "UserId") + .IsUnique() + .HasDatabaseName("ix_student_profiles_tenant_id_user_id"); + + b.ToTable("student_profiles", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("avatar_url"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasMaxLength(320) + .HasColumnType("citext") + .HasColumnName("email"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_at"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyPasswordHash") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("legacy_password_hash"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PasswordMigrationRequired") + .HasColumnType("boolean") + .HasColumnName("password_migration_required"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("phone"); + + b.Property("PrimaryRole") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("primary_role"); + + b.Property("RawProfile") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("raw_profile") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Score") + .HasColumnType("integer") + .HasColumnName("score"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("Username") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_users_email"); + + b.HasIndex("LegacyId") + .IsUnique() + .HasDatabaseName("ix_users_legacy_id"); + + b.HasIndex("Phone") + .IsUnique() + .HasDatabaseName("ix_users_phone"); + + b.HasIndex("Username") + .IsUnique() + .HasDatabaseName("ix_users_username"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.UserIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasMaxLength(320) + .HasColumnType("citext") + .HasColumnName("email"); + + b.Property("OpenId") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("open_id"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("phone"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("ProviderSubject") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("provider_subject"); + + b.Property("SecretPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("secret_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("UnionId") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("union_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_identities"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_identities_user_id"); + + b.HasIndex("Provider", "ProviderSubject") + .IsUnique() + .HasDatabaseName("ix_user_identities_provider_provider_subject"); + + b.ToTable("user_identities", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.AnswerRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AnswerText") + .HasColumnType("text") + .HasColumnName("answer_text"); + + b.Property("AnsweredAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("answered_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsCorrect") + .HasColumnType("boolean") + .HasColumnName("is_correct"); + + b.Property("LegacyCategoryId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_category_id"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyQuestionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_question_id"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("QuestionVersionId") + .HasColumnType("uuid") + .HasColumnName("question_version_id"); + + b.Property("SelectedOptions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("selected_options") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_answer_records"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_answer_records_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_answer_records_user_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_answer_records_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "QuestionId") + .HasDatabaseName("ix_answer_records_tenant_id_question_id"); + + b.HasIndex("TenantId", "QuestionId", "QuestionVersionId") + .HasDatabaseName("ix_answer_records_tenant_id_question_id_question_version_id"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId") + .HasDatabaseName("ix_answer_records_tenant_id_user_id_practice_session_id"); + + b.ToTable("answer_records", null, t => + { + t.HasCheckConstraint("ck_answer_records_version_requires_question", "question_version_id is null or question_id is not null"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.FavoriteQuestion", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source"); + + b.HasKey("TenantId", "UserId", "QuestionId") + .HasName("pk_favorite_questions"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_favorite_questions_user_id"); + + b.HasIndex("TenantId", "QuestionId") + .HasDatabaseName("ix_favorite_questions_tenant_id_question_id"); + + b.ToTable("favorite_questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_at"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mode"); + + b.Property("StartedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at") + .HasDefaultValueSql("now()"); + + b.Property("TargetId") + .HasColumnType("uuid") + .HasColumnName("target_id"); + + b.Property("TargetType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("target_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_practice_sessions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_practice_sessions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "UserId", "Id") + .HasName("ak_practice_sessions_tenant_id_user_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_practice_sessions_user_id"); + + b.HasIndex("TenantId", "UserId", "StartedAt") + .HasDatabaseName("ix_practice_sessions_tenant_id_user_id_started_at"); + + b.ToTable("practice_sessions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.RecentPractice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Color") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("color"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LastAccessAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_access_at"); + + b.Property("LastPracticeAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_practice_at"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PracticeType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("practice_type"); + + b.Property("Progress") + .HasColumnType("integer") + .HasColumnName("progress"); + + b.Property("TargetLegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("target_legacy_id"); + + b.Property("TargetName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("target_name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_recent_practices"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_recent_practices_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_recent_practices_user_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_recent_practices_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "UserId", "LastAccessAt") + .HasDatabaseName("ix_recent_practices_tenant_id_user_id_last_access_at"); + + b.ToTable("recent_practices", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.WrongQuestion", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("LastWrongAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_wrong_at") + .HasDefaultValueSql("now()"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("resolved_at"); + + b.Property("WrongCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("wrong_count"); + + b.HasKey("TenantId", "UserId", "QuestionId") + .HasName("pk_wrong_questions"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_wrong_questions_user_id"); + + b.HasIndex("TenantId", "QuestionId") + .HasDatabaseName("ix_wrong_questions_tenant_id_question_id"); + + b.ToTable("wrong_questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + b.Property("HasVideoExplanation") + .HasColumnType("boolean") + .HasColumnName("has_video_explanation"); + + b.Property("LegacyCategoryId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_category_id"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyNodeId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_node_id"); + + b.Property("LegacySubjectId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_subject_id"); + + b.Property("MediaUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("media_url"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id"); + + b.Property("QuestionBankId") + .HasColumnType("uuid") + .HasColumnName("question_bank_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("SubjectId") + .HasColumnType("uuid") + .HasColumnName("subject_id"); + + b.Property("Tags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("tags") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("type"); + + b.Property("TypeLabel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("type_label"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_questions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_questions_tenant_id_id"); + + b.HasIndex("TenantId", "CategoryId") + .HasDatabaseName("ix_questions_tenant_id_category_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_questions_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "NodeId") + .HasDatabaseName("ix_questions_tenant_id_node_id"); + + b.HasIndex("TenantId", "QuestionBankId") + .HasDatabaseName("ix_questions_tenant_id_question_bank_id"); + + b.HasIndex("TenantId", "SubjectId") + .HasDatabaseName("ix_questions_tenant_id_subject_id"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_questions_tenant_id_id_current_version_id"); + + b.ToTable("questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.QuestionBank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SourceScope") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("source_scope"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_question_banks"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_banks_tenant_id_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_question_banks_tenant_id_region_id"); + + b.ToTable("question_banks", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.QuestionVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AnswerText") + .HasColumnType("text") + .HasColumnName("answer_text"); + + b.Property("CodeLang") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code_lang"); + + b.Property("CodeTemplate") + .HasColumnType("text") + .HasColumnName("code_template"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CorrectOptionIndex") + .HasColumnType("integer") + .HasColumnName("correct_option_index"); + + b.Property("CorrectOptionIndices") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("correct_option_indices") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("Explanation") + .HasColumnType("text") + .HasColumnName("explanation"); + + b.Property("Options") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("options") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("SourceHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("source_hash"); + + b.Property("SubQuestions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("sub_questions") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_question_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "QuestionId", "Id") + .HasName("ak_question_versions_tenant_id_question_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_question_versions_created_by"); + + b.HasIndex("QuestionId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_question_versions_question_id_version_no"); + + b.ToTable("question_versions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BillingStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("billing_status"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegalName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("legal_name"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("mode"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("OwnerUserId") + .HasColumnType("uuid") + .HasColumnName("owner_user_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("citext") + .HasColumnName("slug"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenants"); + + b.HasIndex("LegacyId") + .IsUnique() + .HasDatabaseName("ix_tenants_legacy_id"); + + b.HasIndex("OwnerUserId") + .HasDatabaseName("ix_tenants_owner_user_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_tenants_slug"); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LegacyRole") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("legacy_role"); + + b.Property("Permissions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("permissions") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("role"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tenant_memberships"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_memberships_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tenant_memberships_user_id"); + + b.HasIndex("TenantId", "UserId", "Role") + .IsUnique() + .HasDatabaseName("ix_tenant_memberships_tenant_id_user_id_role"); + + b.ToTable("tenant_memberships", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Category", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_categories_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.ModuleNode", null) + .WithMany() + .HasForeignKey("TenantId", "NodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_categories_module_nodes_tenant_id_node_id"); + + b.HasOne("Tiku.Domain.Catalog.Subject", null) + .WithMany() + .HasForeignKey("TenantId", "SubjectId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_categories_subjects_tenant_id_subject_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Major", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_majors_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_majors_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.School", null) + .WithMany() + .HasForeignKey("TenantId", "SchoolId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_majors_schools_tenant_id_school_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.ModuleNode", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_module_nodes_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.RegionModule", null) + .WithMany() + .HasForeignKey("TenantId", "ModuleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_module_nodes_region_modules_tenant_id_module_id"); + + b.HasOne("Tiku.Domain.Catalog.ModuleNode", null) + .WithMany() + .HasForeignKey("TenantId", "ParentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_module_nodes_module_nodes_tenant_id_parent_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_module_nodes_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Region", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.RegionModule", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_region_modules_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_region_modules_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.School", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_schools_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.RegionModule", null) + .WithMany() + .HasForeignKey("TenantId", "ModuleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_schools_region_modules_tenant_id_module_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_schools_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Subject", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subjects_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Major", null) + .WithMany() + .HasForeignKey("TenantId", "MajorId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_majors_tenant_id_major_id"); + + b.HasOne("Tiku.Domain.Catalog.RegionModule", null) + .WithMany() + .HasForeignKey("TenantId", "ModuleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_region_modules_tenant_id_module_id"); + + b.HasOne("Tiku.Domain.Catalog.ModuleNode", null) + .WithMany() + .HasForeignKey("TenantId", "NodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_module_nodes_tenant_id_node_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.School", null) + .WithMany() + .HasForeignKey("TenantId", "SchoolId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_schools_tenant_id_school_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.StudentProfile", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_profiles_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_profiles_users_user_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_student_profiles_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.Major", null) + .WithMany() + .HasForeignKey("TenantId", "SelectedMajorId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_student_profiles_majors_tenant_id_selected_major_id"); + + b.HasOne("Tiku.Domain.Catalog.School", null) + .WithMany() + .HasForeignKey("TenantId", "SelectedSchoolId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_student_profiles_schools_tenant_id_selected_school_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.UserIdentity", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_identities_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.AnswerRecord", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_answer_records_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_answer_records_users_user_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_answer_records_questions_tenant_id_question_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionVersion", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId", "QuestionVersionId") + .HasPrincipalKey("TenantId", "QuestionId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_answer_records_question_versions_tenant_id_question_id_ques~"); + + b.HasOne("Tiku.Domain.Learning.PracticeSession", null) + .WithMany() + .HasForeignKey("TenantId", "UserId", "PracticeSessionId") + .HasPrincipalKey("TenantId", "UserId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_answer_records_practice_sessions_tenant_id_user_id_practice~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.FavoriteQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_favorite_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_favorite_questions_users_user_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_favorite_questions_questions_tenant_id_question_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSession", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_sessions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_sessions_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.RecentPractice", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_recent_practices_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_recent_practices_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.WrongQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_users_user_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_questions_tenant_id_question_id"); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.Question", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Category", null) + .WithMany() + .HasForeignKey("TenantId", "CategoryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_categories_tenant_id_category_id"); + + b.HasOne("Tiku.Domain.Catalog.ModuleNode", null) + .WithMany() + .HasForeignKey("TenantId", "NodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_module_nodes_tenant_id_node_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionBank", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionBankId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_question_banks_tenant_id_question_bank_id"); + + b.HasOne("Tiku.Domain.Catalog.Subject", null) + .WithMany() + .HasForeignKey("TenantId", "SubjectId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_subjects_tenant_id_subject_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "QuestionId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_question_versions_tenant_id_id_current_version_id"); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.QuestionBank", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_banks_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_banks_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.QuestionVersion", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_question_versions_users_created_by"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_versions_questions_tenant_id_question_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.Tenant", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenants_users_owner_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantMembership", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_memberships_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_memberships_users_user_id"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Tiku.Infrastructure/Persistence/Migrations/20260725205004_InitialCore.cs b/Tiku.Infrastructure/Persistence/Migrations/20260725205004_InitialCore.cs new file mode 100644 index 0000000..8e02f8f --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/20260725205004_InitialCore.cs @@ -0,0 +1,1229 @@ +using System; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Tiku.Infrastructure.Persistence.Migrations +{ + /// + public partial class InitialCore : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:PostgresExtension:citext", ",,"); + + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + legacy_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + username = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + email = table.Column(type: "citext", maxLength: 320, nullable: true), + phone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + name = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + avatar_url = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + primary_role = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + score = table.Column(type: "integer", nullable: false), + last_seen_at = table.Column(type: "timestamp with time zone", nullable: true), + legacy_password_hash = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + password_migration_required = table.Column(type: "boolean", nullable: false), + raw_profile = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_users", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "tenants", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + slug = table.Column(type: "citext", maxLength: 100, nullable: false), + name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + legal_name = table.Column(type: "character varying(300)", maxLength: 300, nullable: true), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + mode = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + billing_status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + owner_user_id = table.Column(type: "uuid", nullable: true), + legacy_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + metadata = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_tenants", x => x.id); + table.ForeignKey( + name: "fk_tenants_users_owner_user_id", + column: x => x.owner_user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "user_identities", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + user_id = table.Column(type: "uuid", nullable: false), + provider = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + provider_subject = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + union_id = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + open_id = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + phone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + email = table.Column(type: "citext", maxLength: 320, nullable: true), + secret_payload = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_user_identities", x => x.id); + table.ForeignKey( + name: "fk_user_identities_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "practice_sessions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + user_id = table.Column(type: "uuid", nullable: false), + mode = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + target_type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + target_id = table.Column(type: "uuid", nullable: true), + started_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + finished_at = table.Column(type: "timestamp with time zone", nullable: true), + metadata = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_practice_sessions", x => x.id); + table.UniqueConstraint("ak_practice_sessions_tenant_id_id", x => new { x.tenant_id, x.id }); + table.UniqueConstraint("ak_practice_sessions_tenant_id_user_id_id", x => new { x.tenant_id, x.user_id, x.id }); + table.ForeignKey( + name: "fk_practice_sessions_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_practice_sessions_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "recent_practices", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + user_id = table.Column(type: "uuid", nullable: true), + legacy_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + practice_type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + target_legacy_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + target_name = table.Column(type: "character varying(300)", maxLength: 300, nullable: true), + progress = table.Column(type: "integer", nullable: false), + color = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + last_access_at = table.Column(type: "timestamp with time zone", nullable: true), + last_practice_at = table.Column(type: "timestamp with time zone", nullable: true), + metadata = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_recent_practices", x => x.id); + table.UniqueConstraint("ak_recent_practices_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_recent_practices_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_recent_practices_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "regions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + legacy_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + code = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + short_name = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + full_name = table.Column(type: "character varying(300)", maxLength: 300, nullable: true), + icon = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + pinyin = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + sort_order = table.Column(type: "integer", nullable: false), + is_hot = table.Column(type: "boolean", nullable: false), + is_active = table.Column(type: "boolean", nullable: false), + config = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_regions", x => x.id); + table.UniqueConstraint("ak_regions_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_regions_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "tenant_memberships", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + user_id = table.Column(type: "uuid", nullable: false), + role = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + permissions = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + legacy_role = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_tenant_memberships", x => x.id); + table.UniqueConstraint("ak_tenant_memberships_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_tenant_memberships_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_tenant_memberships_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "question_banks", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + region_id = table.Column(type: "uuid", nullable: true), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + source_scope = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + metadata = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_question_banks", x => x.id); + table.UniqueConstraint("ak_question_banks_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_question_banks_regions_tenant_id_region_id", + columns: x => new { x.tenant_id, x.region_id }, + principalTable: "regions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_question_banks_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "region_modules", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + region_id = table.Column(type: "uuid", nullable: true), + legacy_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + icon = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + color = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + text_color = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + description = table.Column(type: "text", nullable: true), + route = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + sort_order = table.Column(type: "integer", nullable: false), + is_primary_school_module = table.Column(type: "boolean", nullable: false), + is_active = table.Column(type: "boolean", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_region_modules", x => x.id); + table.UniqueConstraint("ak_region_modules_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_region_modules_regions_tenant_id_region_id", + columns: x => new { x.tenant_id, x.region_id }, + principalTable: "regions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_region_modules_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "module_nodes", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + region_id = table.Column(type: "uuid", nullable: true), + module_id = table.Column(type: "uuid", nullable: true), + parent_id = table.Column(type: "uuid", nullable: true), + legacy_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + legacy_parent_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + legacy_module_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + type = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + path = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + sort_order = table.Column(type: "integer", nullable: false), + is_active = table.Column(type: "boolean", nullable: false), + metadata = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_module_nodes", x => x.id); + table.UniqueConstraint("ak_module_nodes_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_module_nodes_module_nodes_tenant_id_parent_id", + columns: x => new { x.tenant_id, x.parent_id }, + principalTable: "module_nodes", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_module_nodes_region_modules_tenant_id_module_id", + columns: x => new { x.tenant_id, x.module_id }, + principalTable: "region_modules", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_module_nodes_regions_tenant_id_region_id", + columns: x => new { x.tenant_id, x.region_id }, + principalTable: "regions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_module_nodes_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "schools", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + region_id = table.Column(type: "uuid", nullable: true), + module_id = table.Column(type: "uuid", nullable: true), + legacy_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + professional_exam_date = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + metadata = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_schools", x => x.id); + table.UniqueConstraint("ak_schools_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_schools_region_modules_tenant_id_module_id", + columns: x => new { x.tenant_id, x.module_id }, + principalTable: "region_modules", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_schools_regions_tenant_id_region_id", + columns: x => new { x.tenant_id, x.region_id }, + principalTable: "regions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_schools_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "majors", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + region_id = table.Column(type: "uuid", nullable: true), + school_id = table.Column(type: "uuid", nullable: true), + legacy_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + description = table.Column(type: "text", nullable: true), + study_tips = table.Column(type: "text", nullable: true), + sort_order = table.Column(type: "integer", nullable: false), + is_active = table.Column(type: "boolean", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_majors", x => x.id); + table.UniqueConstraint("ak_majors_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_majors_regions_tenant_id_region_id", + columns: x => new { x.tenant_id, x.region_id }, + principalTable: "regions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_majors_schools_tenant_id_school_id", + columns: x => new { x.tenant_id, x.school_id }, + principalTable: "schools", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_majors_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "student_profiles", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + user_id = table.Column(type: "uuid", nullable: false), + legacy_user_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + region_id = table.Column(type: "uuid", nullable: true), + selected_school_id = table.Column(type: "uuid", nullable: true), + selected_major_id = table.Column(type: "uuid", nullable: true), + questions_answered_today = table.Column(type: "integer", nullable: false), + mastered_words_count = table.Column(type: "integer", nullable: false), + last_check_in_date = table.Column(type: "date", nullable: true), + stats = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + progress = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + module_selections = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + recent_activities = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_student_profiles", x => x.id); + table.UniqueConstraint("ak_student_profiles_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_student_profiles_majors_tenant_id_selected_major_id", + columns: x => new { x.tenant_id, x.selected_major_id }, + principalTable: "majors", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_student_profiles_regions_tenant_id_region_id", + columns: x => new { x.tenant_id, x.region_id }, + principalTable: "regions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_student_profiles_schools_tenant_id_selected_school_id", + columns: x => new { x.tenant_id, x.selected_school_id }, + principalTable: "schools", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_student_profiles_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_student_profiles_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "subjects", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + region_id = table.Column(type: "uuid", nullable: true), + module_id = table.Column(type: "uuid", nullable: true), + school_id = table.Column(type: "uuid", nullable: true), + major_id = table.Column(type: "uuid", nullable: true), + node_id = table.Column(type: "uuid", nullable: true), + legacy_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + type = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + major_legacy_ids = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"), + icon = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + description = table.Column(type: "text", nullable: true), + stats = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), + sort_order = table.Column(type: "integer", nullable: false), + is_active = table.Column(type: "boolean", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_subjects", x => x.id); + table.UniqueConstraint("ak_subjects_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_subjects_majors_tenant_id_major_id", + columns: x => new { x.tenant_id, x.major_id }, + principalTable: "majors", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_subjects_module_nodes_tenant_id_node_id", + columns: x => new { x.tenant_id, x.node_id }, + principalTable: "module_nodes", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_subjects_region_modules_tenant_id_module_id", + columns: x => new { x.tenant_id, x.module_id }, + principalTable: "region_modules", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_subjects_regions_tenant_id_region_id", + columns: x => new { x.tenant_id, x.region_id }, + principalTable: "regions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_subjects_schools_tenant_id_school_id", + columns: x => new { x.tenant_id, x.school_id }, + principalTable: "schools", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_subjects_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "categories", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + subject_id = table.Column(type: "uuid", nullable: true), + node_id = table.Column(type: "uuid", nullable: true), + legacy_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + category_type = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + sort_order = table.Column(type: "integer", nullable: false), + svip_question_limit = table.Column(type: "integer", nullable: true), + is_active = table.Column(type: "boolean", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_categories", x => x.id); + table.UniqueConstraint("ak_categories_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_categories_module_nodes_tenant_id_node_id", + columns: x => new { x.tenant_id, x.node_id }, + principalTable: "module_nodes", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_categories_subjects_tenant_id_subject_id", + columns: x => new { x.tenant_id, x.subject_id }, + principalTable: "subjects", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_categories_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "answer_records", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + user_id = table.Column(type: "uuid", nullable: false), + question_id = table.Column(type: "uuid", nullable: true), + question_version_id = table.Column(type: "uuid", nullable: true), + practice_session_id = table.Column(type: "uuid", nullable: true), + legacy_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + legacy_question_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + legacy_category_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + selected_options = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"), + answer_text = table.Column(type: "text", nullable: true), + is_correct = table.Column(type: "boolean", nullable: true), + answered_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + tenant_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_answer_records", x => x.id); + table.UniqueConstraint("ak_answer_records_tenant_id_id", x => new { x.tenant_id, x.id }); + table.CheckConstraint("ck_answer_records_version_requires_question", "question_version_id is null or question_id is not null"); + table.ForeignKey( + name: "fk_answer_records_practice_sessions_tenant_id_user_id_practice~", + columns: x => new { x.tenant_id, x.user_id, x.practice_session_id }, + principalTable: "practice_sessions", + principalColumns: new[] { "tenant_id", "user_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_answer_records_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_answer_records_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "favorite_questions", + columns: table => new + { + tenant_id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: false), + question_id = table.Column(type: "uuid", nullable: false), + source = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_favorite_questions", x => new { x.tenant_id, x.user_id, x.question_id }); + table.ForeignKey( + name: "fk_favorite_questions_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_favorite_questions_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "question_versions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + tenant_id = table.Column(type: "uuid", nullable: false), + question_id = table.Column(type: "uuid", nullable: false), + version_no = table.Column(type: "integer", nullable: false), + content = table.Column(type: "text", nullable: true), + options = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"), + correct_option_index = table.Column(type: "integer", nullable: true), + correct_option_indices = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"), + answer_text = table.Column(type: "text", nullable: true), + explanation = table.Column(type: "text", nullable: true), + sub_questions = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"), + code_lang = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + code_template = table.Column(type: "text", nullable: true), + source_hash = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + created_by = table.Column(type: "uuid", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_question_versions", x => x.id); + table.UniqueConstraint("ak_question_versions_tenant_id_id", x => new { x.tenant_id, x.id }); + table.UniqueConstraint("ak_question_versions_tenant_id_question_id_id", x => new { x.tenant_id, x.question_id, x.id }); + table.ForeignKey( + name: "fk_question_versions_users_created_by", + column: x => x.created_by, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "questions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + question_bank_id = table.Column(type: "uuid", nullable: true), + subject_id = table.Column(type: "uuid", nullable: true), + category_id = table.Column(type: "uuid", nullable: true), + node_id = table.Column(type: "uuid", nullable: true), + legacy_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + legacy_subject_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + legacy_category_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + legacy_node_id = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + type = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + type_label = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + difficulty = table.Column(type: "integer", nullable: true), + tags = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"), + media_url = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + has_video_explanation = table.Column(type: "boolean", nullable: false), + status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + current_version_id = table.Column(type: "uuid", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("pk_questions", x => x.id); + table.UniqueConstraint("ak_questions_tenant_id_id", x => new { x.tenant_id, x.id }); + table.ForeignKey( + name: "fk_questions_categories_tenant_id_category_id", + columns: x => new { x.tenant_id, x.category_id }, + principalTable: "categories", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_questions_module_nodes_tenant_id_node_id", + columns: x => new { x.tenant_id, x.node_id }, + principalTable: "module_nodes", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_questions_question_banks_tenant_id_question_bank_id", + columns: x => new { x.tenant_id, x.question_bank_id }, + principalTable: "question_banks", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_questions_question_versions_tenant_id_id_current_version_id", + columns: x => new { x.tenant_id, x.id, x.current_version_id }, + principalTable: "question_versions", + principalColumns: new[] { "tenant_id", "question_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_questions_subjects_tenant_id_subject_id", + columns: x => new { x.tenant_id, x.subject_id }, + principalTable: "subjects", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_questions_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "wrong_questions", + columns: table => new + { + tenant_id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: false), + question_id = table.Column(type: "uuid", nullable: false), + wrong_count = table.Column(type: "integer", nullable: false, defaultValue: 1), + last_wrong_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + resolved_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_wrong_questions", x => new { x.tenant_id, x.user_id, x.question_id }); + table.ForeignKey( + name: "fk_wrong_questions_questions_tenant_id_question_id", + columns: x => new { x.tenant_id, x.question_id }, + principalTable: "questions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_wrong_questions_tenants_tenant_id", + column: x => x.tenant_id, + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_wrong_questions_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_answer_records_tenant_id_legacy_id", + table: "answer_records", + columns: new[] { "tenant_id", "legacy_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_answer_records_tenant_id_question_id", + table: "answer_records", + columns: new[] { "tenant_id", "question_id" }); + + migrationBuilder.CreateIndex( + name: "ix_answer_records_tenant_id_question_id_question_version_id", + table: "answer_records", + columns: new[] { "tenant_id", "question_id", "question_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_answer_records_tenant_id_user_id_practice_session_id", + table: "answer_records", + columns: new[] { "tenant_id", "user_id", "practice_session_id" }); + + migrationBuilder.CreateIndex( + name: "ix_answer_records_user_id", + table: "answer_records", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_categories_tenant_id_legacy_id", + table: "categories", + columns: new[] { "tenant_id", "legacy_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_categories_tenant_id_node_id", + table: "categories", + columns: new[] { "tenant_id", "node_id" }); + + migrationBuilder.CreateIndex( + name: "ix_categories_tenant_id_subject_id", + table: "categories", + columns: new[] { "tenant_id", "subject_id" }); + + migrationBuilder.CreateIndex( + name: "ix_favorite_questions_tenant_id_question_id", + table: "favorite_questions", + columns: new[] { "tenant_id", "question_id" }); + + migrationBuilder.CreateIndex( + name: "ix_favorite_questions_user_id", + table: "favorite_questions", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_majors_tenant_id_legacy_id", + table: "majors", + columns: new[] { "tenant_id", "legacy_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_majors_tenant_id_region_id", + table: "majors", + columns: new[] { "tenant_id", "region_id" }); + + migrationBuilder.CreateIndex( + name: "ix_majors_tenant_id_school_id", + table: "majors", + columns: new[] { "tenant_id", "school_id" }); + + migrationBuilder.CreateIndex( + name: "ix_module_nodes_tenant_id_legacy_id", + table: "module_nodes", + columns: new[] { "tenant_id", "legacy_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_module_nodes_tenant_id_module_id", + table: "module_nodes", + columns: new[] { "tenant_id", "module_id" }); + + migrationBuilder.CreateIndex( + name: "ix_module_nodes_tenant_id_parent_id", + table: "module_nodes", + columns: new[] { "tenant_id", "parent_id" }); + + migrationBuilder.CreateIndex( + name: "ix_module_nodes_tenant_id_region_id", + table: "module_nodes", + columns: new[] { "tenant_id", "region_id" }); + + migrationBuilder.CreateIndex( + name: "ix_practice_sessions_tenant_id_user_id_started_at", + table: "practice_sessions", + columns: new[] { "tenant_id", "user_id", "started_at" }); + + migrationBuilder.CreateIndex( + name: "ix_practice_sessions_user_id", + table: "practice_sessions", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_question_banks_tenant_id_region_id", + table: "question_banks", + columns: new[] { "tenant_id", "region_id" }); + + migrationBuilder.CreateIndex( + name: "ix_question_versions_created_by", + table: "question_versions", + column: "created_by"); + + migrationBuilder.CreateIndex( + name: "ix_question_versions_question_id_version_no", + table: "question_versions", + columns: new[] { "question_id", "version_no" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_questions_tenant_id_category_id", + table: "questions", + columns: new[] { "tenant_id", "category_id" }); + + migrationBuilder.CreateIndex( + name: "ix_questions_tenant_id_id_current_version_id", + table: "questions", + columns: new[] { "tenant_id", "id", "current_version_id" }); + + migrationBuilder.CreateIndex( + name: "ix_questions_tenant_id_legacy_id", + table: "questions", + columns: new[] { "tenant_id", "legacy_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_questions_tenant_id_node_id", + table: "questions", + columns: new[] { "tenant_id", "node_id" }); + + migrationBuilder.CreateIndex( + name: "ix_questions_tenant_id_question_bank_id", + table: "questions", + columns: new[] { "tenant_id", "question_bank_id" }); + + migrationBuilder.CreateIndex( + name: "ix_questions_tenant_id_subject_id", + table: "questions", + columns: new[] { "tenant_id", "subject_id" }); + + migrationBuilder.CreateIndex( + name: "ix_recent_practices_tenant_id_legacy_id", + table: "recent_practices", + columns: new[] { "tenant_id", "legacy_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_recent_practices_tenant_id_user_id_last_access_at", + table: "recent_practices", + columns: new[] { "tenant_id", "user_id", "last_access_at" }); + + migrationBuilder.CreateIndex( + name: "ix_recent_practices_user_id", + table: "recent_practices", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_region_modules_tenant_id_legacy_id", + table: "region_modules", + columns: new[] { "tenant_id", "legacy_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_region_modules_tenant_id_region_id", + table: "region_modules", + columns: new[] { "tenant_id", "region_id" }); + + migrationBuilder.CreateIndex( + name: "ix_regions_tenant_id_legacy_id", + table: "regions", + columns: new[] { "tenant_id", "legacy_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_schools_tenant_id_legacy_id", + table: "schools", + columns: new[] { "tenant_id", "legacy_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_schools_tenant_id_module_id", + table: "schools", + columns: new[] { "tenant_id", "module_id" }); + + migrationBuilder.CreateIndex( + name: "ix_schools_tenant_id_region_id", + table: "schools", + columns: new[] { "tenant_id", "region_id" }); + + migrationBuilder.CreateIndex( + name: "ix_student_profiles_tenant_id_region_id", + table: "student_profiles", + columns: new[] { "tenant_id", "region_id" }); + + migrationBuilder.CreateIndex( + name: "ix_student_profiles_tenant_id_selected_major_id", + table: "student_profiles", + columns: new[] { "tenant_id", "selected_major_id" }); + + migrationBuilder.CreateIndex( + name: "ix_student_profiles_tenant_id_selected_school_id", + table: "student_profiles", + columns: new[] { "tenant_id", "selected_school_id" }); + + migrationBuilder.CreateIndex( + name: "ix_student_profiles_tenant_id_user_id", + table: "student_profiles", + columns: new[] { "tenant_id", "user_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_student_profiles_user_id", + table: "student_profiles", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_subjects_tenant_id_legacy_id", + table: "subjects", + columns: new[] { "tenant_id", "legacy_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_subjects_tenant_id_major_id", + table: "subjects", + columns: new[] { "tenant_id", "major_id" }); + + migrationBuilder.CreateIndex( + name: "ix_subjects_tenant_id_module_id", + table: "subjects", + columns: new[] { "tenant_id", "module_id" }); + + migrationBuilder.CreateIndex( + name: "ix_subjects_tenant_id_node_id", + table: "subjects", + columns: new[] { "tenant_id", "node_id" }); + + migrationBuilder.CreateIndex( + name: "ix_subjects_tenant_id_region_id", + table: "subjects", + columns: new[] { "tenant_id", "region_id" }); + + migrationBuilder.CreateIndex( + name: "ix_subjects_tenant_id_school_id", + table: "subjects", + columns: new[] { "tenant_id", "school_id" }); + + migrationBuilder.CreateIndex( + name: "ix_tenant_memberships_tenant_id_user_id_role", + table: "tenant_memberships", + columns: new[] { "tenant_id", "user_id", "role" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_tenant_memberships_user_id", + table: "tenant_memberships", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_tenants_legacy_id", + table: "tenants", + column: "legacy_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_tenants_owner_user_id", + table: "tenants", + column: "owner_user_id"); + + migrationBuilder.CreateIndex( + name: "ix_tenants_slug", + table: "tenants", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_user_identities_provider_provider_subject", + table: "user_identities", + columns: new[] { "provider", "provider_subject" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_user_identities_user_id", + table: "user_identities", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_users_email", + table: "users", + column: "email", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_users_legacy_id", + table: "users", + column: "legacy_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_users_phone", + table: "users", + column: "phone", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_users_username", + table: "users", + column: "username", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_wrong_questions_tenant_id_question_id", + table: "wrong_questions", + columns: new[] { "tenant_id", "question_id" }); + + migrationBuilder.CreateIndex( + name: "ix_wrong_questions_user_id", + table: "wrong_questions", + column: "user_id"); + + migrationBuilder.AddForeignKey( + name: "fk_answer_records_question_versions_tenant_id_question_id_ques~", + table: "answer_records", + columns: new[] { "tenant_id", "question_id", "question_version_id" }, + principalTable: "question_versions", + principalColumns: new[] { "tenant_id", "question_id", "id" }, + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "fk_answer_records_questions_tenant_id_question_id", + table: "answer_records", + columns: new[] { "tenant_id", "question_id" }, + principalTable: "questions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "fk_favorite_questions_questions_tenant_id_question_id", + table: "favorite_questions", + columns: new[] { "tenant_id", "question_id" }, + principalTable: "questions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_question_versions_questions_tenant_id_question_id", + table: "question_versions", + columns: new[] { "tenant_id", "question_id" }, + principalTable: "questions", + principalColumns: new[] { "tenant_id", "id" }, + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_questions_question_versions_tenant_id_id_current_version_id", + table: "questions"); + + migrationBuilder.DropTable( + name: "answer_records"); + + migrationBuilder.DropTable( + name: "favorite_questions"); + + migrationBuilder.DropTable( + name: "recent_practices"); + + migrationBuilder.DropTable( + name: "student_profiles"); + + migrationBuilder.DropTable( + name: "tenant_memberships"); + + migrationBuilder.DropTable( + name: "user_identities"); + + migrationBuilder.DropTable( + name: "wrong_questions"); + + migrationBuilder.DropTable( + name: "practice_sessions"); + + migrationBuilder.DropTable( + name: "question_versions"); + + migrationBuilder.DropTable( + name: "questions"); + + migrationBuilder.DropTable( + name: "categories"); + + migrationBuilder.DropTable( + name: "question_banks"); + + migrationBuilder.DropTable( + name: "subjects"); + + migrationBuilder.DropTable( + name: "majors"); + + migrationBuilder.DropTable( + name: "module_nodes"); + + migrationBuilder.DropTable( + name: "schools"); + + migrationBuilder.DropTable( + name: "region_modules"); + + migrationBuilder.DropTable( + name: "regions"); + + migrationBuilder.DropTable( + name: "tenants"); + + migrationBuilder.DropTable( + name: "users"); + } + } +} diff --git a/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs b/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs new file mode 100644 index 0000000..cda8b06 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/TikuDbContextModelSnapshot.cs @@ -0,0 +1,2173 @@ +// +using System; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Tiku.Infrastructure.Persistence; + +#nullable disable + +namespace Tiku.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(TikuDbContext))] + partial class TikuDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Tiku.Domain.Catalog.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CategoryType") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("category_type"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("SubjectId") + .HasColumnType("uuid") + .HasColumnName("subject_id"); + + b.Property("SvipQuestionLimit") + .HasColumnType("integer") + .HasColumnName("svip_question_limit"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_categories"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_categories_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_categories_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "NodeId") + .HasDatabaseName("ix_categories_tenant_id_node_id"); + + b.HasIndex("TenantId", "SubjectId") + .HasDatabaseName("ix_categories_tenant_id_subject_id"); + + b.ToTable("categories", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Major", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SchoolId") + .HasColumnType("uuid") + .HasColumnName("school_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("StudyTips") + .HasColumnType("text") + .HasColumnName("study_tips"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_majors"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_majors_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_majors_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_majors_tenant_id_region_id"); + + b.HasIndex("TenantId", "SchoolId") + .HasDatabaseName("ix_majors_tenant_id_school_id"); + + b.ToTable("majors", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.ModuleNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyModuleId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_module_id"); + + b.Property("LegacyParentId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_parent_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("Path") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("path"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_module_nodes"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_module_nodes_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_module_nodes_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "ModuleId") + .HasDatabaseName("ix_module_nodes_tenant_id_module_id"); + + b.HasIndex("TenantId", "ParentId") + .HasDatabaseName("ix_module_nodes_tenant_id_parent_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_module_nodes_tenant_id_region_id"); + + b.ToTable("module_nodes", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("Config") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("config") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("FullName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("full_name"); + + b.Property("Icon") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("icon"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsHot") + .HasColumnType("boolean") + .HasColumnName("is_hot"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Pinyin") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("pinyin"); + + b.Property("ShortName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("short_name"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_regions_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_regions_tenant_id_legacy_id"); + + b.ToTable("regions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.RegionModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Color") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("color"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Icon") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("icon"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsPrimarySchoolModule") + .HasColumnType("boolean") + .HasColumnName("is_primary_school_module"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("Route") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("route"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TextColor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("text_color"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_region_modules"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_region_modules_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_region_modules_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_region_modules_tenant_id_region_id"); + + b.ToTable("region_modules", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.School", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("ProfessionalExamDate") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("professional_exam_date"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_schools"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_schools_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_schools_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "ModuleId") + .HasDatabaseName("ix_schools_tenant_id_module_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_schools_tenant_id_region_id"); + + b.ToTable("schools", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Subject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Icon") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("icon"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("MajorId") + .HasColumnType("uuid") + .HasColumnName("major_id"); + + b.Property("MajorLegacyIds") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("major_legacy_ids") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SchoolId") + .HasColumnType("uuid") + .HasColumnName("school_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("Stats") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("stats") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Type") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_subjects"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_subjects_tenant_id_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_subjects_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "MajorId") + .HasDatabaseName("ix_subjects_tenant_id_major_id"); + + b.HasIndex("TenantId", "ModuleId") + .HasDatabaseName("ix_subjects_tenant_id_module_id"); + + b.HasIndex("TenantId", "NodeId") + .HasDatabaseName("ix_subjects_tenant_id_node_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_subjects_tenant_id_region_id"); + + b.HasIndex("TenantId", "SchoolId") + .HasDatabaseName("ix_subjects_tenant_id_school_id"); + + b.ToTable("subjects", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.StudentProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LastCheckInDate") + .HasColumnType("date") + .HasColumnName("last_check_in_date"); + + b.Property("LegacyUserId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_user_id"); + + b.Property("MasteredWordsCount") + .HasColumnType("integer") + .HasColumnName("mastered_words_count"); + + b.Property("ModuleSelections") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("module_selections") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Progress") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("progress") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("QuestionsAnsweredToday") + .HasColumnType("integer") + .HasColumnName("questions_answered_today"); + + b.Property("RecentActivities") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("recent_activities") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SelectedMajorId") + .HasColumnType("uuid") + .HasColumnName("selected_major_id"); + + b.Property("SelectedSchoolId") + .HasColumnType("uuid") + .HasColumnName("selected_school_id"); + + b.Property("Stats") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("stats") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_student_profiles"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_student_profiles_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_student_profiles_user_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_student_profiles_tenant_id_region_id"); + + b.HasIndex("TenantId", "SelectedMajorId") + .HasDatabaseName("ix_student_profiles_tenant_id_selected_major_id"); + + b.HasIndex("TenantId", "SelectedSchoolId") + .HasDatabaseName("ix_student_profiles_tenant_id_selected_school_id"); + + b.HasIndex("TenantId", "UserId") + .IsUnique() + .HasDatabaseName("ix_student_profiles_tenant_id_user_id"); + + b.ToTable("student_profiles", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("avatar_url"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasMaxLength(320) + .HasColumnType("citext") + .HasColumnName("email"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_at"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyPasswordHash") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("legacy_password_hash"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PasswordMigrationRequired") + .HasColumnType("boolean") + .HasColumnName("password_migration_required"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("phone"); + + b.Property("PrimaryRole") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("primary_role"); + + b.Property("RawProfile") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("raw_profile") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Score") + .HasColumnType("integer") + .HasColumnName("score"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("Username") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_users_email"); + + b.HasIndex("LegacyId") + .IsUnique() + .HasDatabaseName("ix_users_legacy_id"); + + b.HasIndex("Phone") + .IsUnique() + .HasDatabaseName("ix_users_phone"); + + b.HasIndex("Username") + .IsUnique() + .HasDatabaseName("ix_users_username"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.UserIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasMaxLength(320) + .HasColumnType("citext") + .HasColumnName("email"); + + b.Property("OpenId") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("open_id"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("phone"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("provider"); + + b.Property("ProviderSubject") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("provider_subject"); + + b.Property("SecretPayload") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("secret_payload") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("UnionId") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("union_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_identities"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_identities_user_id"); + + b.HasIndex("Provider", "ProviderSubject") + .IsUnique() + .HasDatabaseName("ix_user_identities_provider_provider_subject"); + + b.ToTable("user_identities", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.AnswerRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AnswerText") + .HasColumnType("text") + .HasColumnName("answer_text"); + + b.Property("AnsweredAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("answered_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("IsCorrect") + .HasColumnType("boolean") + .HasColumnName("is_correct"); + + b.Property("LegacyCategoryId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_category_id"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyQuestionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_question_id"); + + b.Property("PracticeSessionId") + .HasColumnType("uuid") + .HasColumnName("practice_session_id"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("QuestionVersionId") + .HasColumnType("uuid") + .HasColumnName("question_version_id"); + + b.Property("SelectedOptions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("selected_options") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_answer_records"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_answer_records_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_answer_records_user_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_answer_records_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "QuestionId") + .HasDatabaseName("ix_answer_records_tenant_id_question_id"); + + b.HasIndex("TenantId", "QuestionId", "QuestionVersionId") + .HasDatabaseName("ix_answer_records_tenant_id_question_id_question_version_id"); + + b.HasIndex("TenantId", "UserId", "PracticeSessionId") + .HasDatabaseName("ix_answer_records_tenant_id_user_id_practice_session_id"); + + b.ToTable("answer_records", null, t => + { + t.HasCheckConstraint("ck_answer_records_version_requires_question", "question_version_id is null or question_id is not null"); + }); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.FavoriteQuestion", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source"); + + b.HasKey("TenantId", "UserId", "QuestionId") + .HasName("pk_favorite_questions"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_favorite_questions_user_id"); + + b.HasIndex("TenantId", "QuestionId") + .HasDatabaseName("ix_favorite_questions_tenant_id_question_id"); + + b.ToTable("favorite_questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_at"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mode"); + + b.Property("StartedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at") + .HasDefaultValueSql("now()"); + + b.Property("TargetId") + .HasColumnType("uuid") + .HasColumnName("target_id"); + + b.Property("TargetType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("target_type"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_practice_sessions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_practice_sessions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "UserId", "Id") + .HasName("ak_practice_sessions_tenant_id_user_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_practice_sessions_user_id"); + + b.HasIndex("TenantId", "UserId", "StartedAt") + .HasDatabaseName("ix_practice_sessions_tenant_id_user_id_started_at"); + + b.ToTable("practice_sessions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.RecentPractice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Color") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("color"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LastAccessAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_access_at"); + + b.Property("LastPracticeAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_practice_at"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PracticeType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("practice_type"); + + b.Property("Progress") + .HasColumnType("integer") + .HasColumnName("progress"); + + b.Property("TargetLegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("target_legacy_id"); + + b.Property("TargetName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("target_name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_recent_practices"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_recent_practices_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_recent_practices_user_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_recent_practices_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "UserId", "LastAccessAt") + .HasDatabaseName("ix_recent_practices_tenant_id_user_id_last_access_at"); + + b.ToTable("recent_practices", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.WrongQuestion", b => + { + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("LastWrongAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_wrong_at") + .HasDefaultValueSql("now()"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("resolved_at"); + + b.Property("WrongCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("wrong_count"); + + b.HasKey("TenantId", "UserId", "QuestionId") + .HasName("pk_wrong_questions"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_wrong_questions_user_id"); + + b.HasIndex("TenantId", "QuestionId") + .HasDatabaseName("ix_wrong_questions_tenant_id_question_id"); + + b.ToTable("wrong_questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CurrentVersionId") + .HasColumnType("uuid") + .HasColumnName("current_version_id"); + + b.Property("Difficulty") + .HasColumnType("integer") + .HasColumnName("difficulty"); + + b.Property("HasVideoExplanation") + .HasColumnType("boolean") + .HasColumnName("has_video_explanation"); + + b.Property("LegacyCategoryId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_category_id"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegacyNodeId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_node_id"); + + b.Property("LegacySubjectId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_subject_id"); + + b.Property("MediaUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("media_url"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id"); + + b.Property("QuestionBankId") + .HasColumnType("uuid") + .HasColumnName("question_bank_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("SubjectId") + .HasColumnType("uuid") + .HasColumnName("subject_id"); + + b.Property("Tags") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("tags") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("type"); + + b.Property("TypeLabel") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("type_label"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_questions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_questions_tenant_id_id"); + + b.HasIndex("TenantId", "CategoryId") + .HasDatabaseName("ix_questions_tenant_id_category_id"); + + b.HasIndex("TenantId", "LegacyId") + .IsUnique() + .HasDatabaseName("ix_questions_tenant_id_legacy_id"); + + b.HasIndex("TenantId", "NodeId") + .HasDatabaseName("ix_questions_tenant_id_node_id"); + + b.HasIndex("TenantId", "QuestionBankId") + .HasDatabaseName("ix_questions_tenant_id_question_bank_id"); + + b.HasIndex("TenantId", "SubjectId") + .HasDatabaseName("ix_questions_tenant_id_subject_id"); + + b.HasIndex("TenantId", "Id", "CurrentVersionId") + .HasDatabaseName("ix_questions_tenant_id_id_current_version_id"); + + b.ToTable("questions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.QuestionBank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("SourceScope") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("source_scope"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_question_banks"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_banks_tenant_id_id"); + + b.HasIndex("TenantId", "RegionId") + .HasDatabaseName("ix_question_banks_tenant_id_region_id"); + + b.ToTable("question_banks", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.QuestionVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AnswerText") + .HasColumnType("text") + .HasColumnName("answer_text"); + + b.Property("CodeLang") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code_lang"); + + b.Property("CodeTemplate") + .HasColumnType("text") + .HasColumnName("code_template"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CorrectOptionIndex") + .HasColumnType("integer") + .HasColumnName("correct_option_index"); + + b.Property("CorrectOptionIndices") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("correct_option_indices") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property("Explanation") + .HasColumnType("text") + .HasColumnName("explanation"); + + b.Property("Options") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("options") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("QuestionId") + .HasColumnType("uuid") + .HasColumnName("question_id"); + + b.Property("SourceHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("source_hash"); + + b.Property("SubQuestions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("sub_questions") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("VersionNo") + .HasColumnType("integer") + .HasColumnName("version_no"); + + b.HasKey("Id") + .HasName("pk_question_versions"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_question_versions_tenant_id_id"); + + b.HasAlternateKey("TenantId", "QuestionId", "Id") + .HasName("ak_question_versions_tenant_id_question_id_id"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("ix_question_versions_created_by"); + + b.HasIndex("QuestionId", "VersionNo") + .IsUnique() + .HasDatabaseName("ix_question_versions_question_id_version_no"); + + b.ToTable("question_versions", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BillingStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("billing_status"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LegacyId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("legacy_id"); + + b.Property("LegalName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("legal_name"); + + b.Property("Metadata") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("metadata") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("mode"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("OwnerUserId") + .HasColumnType("uuid") + .HasColumnName("owner_user_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("citext") + .HasColumnName("slug"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.HasKey("Id") + .HasName("pk_tenants"); + + b.HasIndex("LegacyId") + .IsUnique() + .HasDatabaseName("ix_tenants_legacy_id"); + + b.HasIndex("OwnerUserId") + .HasDatabaseName("ix_tenants_owner_user_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_tenants_slug"); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("LegacyRole") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("legacy_role"); + + b.Property("Permissions") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("permissions") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("role"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tenant_memberships"); + + b.HasAlternateKey("TenantId", "Id") + .HasName("ak_tenant_memberships_tenant_id_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tenant_memberships_user_id"); + + b.HasIndex("TenantId", "UserId", "Role") + .IsUnique() + .HasDatabaseName("ix_tenant_memberships_tenant_id_user_id_role"); + + b.ToTable("tenant_memberships", (string)null); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Category", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_categories_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.ModuleNode", null) + .WithMany() + .HasForeignKey("TenantId", "NodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_categories_module_nodes_tenant_id_node_id"); + + b.HasOne("Tiku.Domain.Catalog.Subject", null) + .WithMany() + .HasForeignKey("TenantId", "SubjectId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_categories_subjects_tenant_id_subject_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Major", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_majors_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_majors_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.School", null) + .WithMany() + .HasForeignKey("TenantId", "SchoolId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_majors_schools_tenant_id_school_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.ModuleNode", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_module_nodes_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.RegionModule", null) + .WithMany() + .HasForeignKey("TenantId", "ModuleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_module_nodes_region_modules_tenant_id_module_id"); + + b.HasOne("Tiku.Domain.Catalog.ModuleNode", null) + .WithMany() + .HasForeignKey("TenantId", "ParentId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_module_nodes_module_nodes_tenant_id_parent_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_module_nodes_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Region", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_tenants_tenant_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.RegionModule", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_region_modules_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_region_modules_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.School", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_schools_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.RegionModule", null) + .WithMany() + .HasForeignKey("TenantId", "ModuleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_schools_region_modules_tenant_id_module_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_schools_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Catalog.Subject", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subjects_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Major", null) + .WithMany() + .HasForeignKey("TenantId", "MajorId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_majors_tenant_id_major_id"); + + b.HasOne("Tiku.Domain.Catalog.RegionModule", null) + .WithMany() + .HasForeignKey("TenantId", "ModuleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_region_modules_tenant_id_module_id"); + + b.HasOne("Tiku.Domain.Catalog.ModuleNode", null) + .WithMany() + .HasForeignKey("TenantId", "NodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_module_nodes_tenant_id_node_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.School", null) + .WithMany() + .HasForeignKey("TenantId", "SchoolId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subjects_schools_tenant_id_school_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.StudentProfile", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_profiles_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_student_profiles_users_user_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_student_profiles_regions_tenant_id_region_id"); + + b.HasOne("Tiku.Domain.Catalog.Major", null) + .WithMany() + .HasForeignKey("TenantId", "SelectedMajorId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_student_profiles_majors_tenant_id_selected_major_id"); + + b.HasOne("Tiku.Domain.Catalog.School", null) + .WithMany() + .HasForeignKey("TenantId", "SelectedSchoolId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_student_profiles_schools_tenant_id_selected_school_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Identity.UserIdentity", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_identities_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.AnswerRecord", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_answer_records_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_answer_records_users_user_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_answer_records_questions_tenant_id_question_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionVersion", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId", "QuestionVersionId") + .HasPrincipalKey("TenantId", "QuestionId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_answer_records_question_versions_tenant_id_question_id_ques~"); + + b.HasOne("Tiku.Domain.Learning.PracticeSession", null) + .WithMany() + .HasForeignKey("TenantId", "UserId", "PracticeSessionId") + .HasPrincipalKey("TenantId", "UserId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_answer_records_practice_sessions_tenant_id_user_id_practice~"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.FavoriteQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_favorite_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_favorite_questions_users_user_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_favorite_questions_questions_tenant_id_question_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.PracticeSession", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_sessions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_practice_sessions_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.RecentPractice", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_recent_practices_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_recent_practices_users_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Learning.WrongQuestion", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_users_user_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wrong_questions_questions_tenant_id_question_id"); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.Question", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_questions_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Category", null) + .WithMany() + .HasForeignKey("TenantId", "CategoryId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_categories_tenant_id_category_id"); + + b.HasOne("Tiku.Domain.Catalog.ModuleNode", null) + .WithMany() + .HasForeignKey("TenantId", "NodeId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_module_nodes_tenant_id_node_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionBank", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionBankId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_question_banks_tenant_id_question_bank_id"); + + b.HasOne("Tiku.Domain.Catalog.Subject", null) + .WithMany() + .HasForeignKey("TenantId", "SubjectId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_subjects_tenant_id_subject_id"); + + b.HasOne("Tiku.Domain.QuestionBanks.QuestionVersion", null) + .WithMany() + .HasForeignKey("TenantId", "Id", "CurrentVersionId") + .HasPrincipalKey("TenantId", "QuestionId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_questions_question_versions_tenant_id_id_current_version_id"); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.QuestionBank", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_banks_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Catalog.Region", null) + .WithMany() + .HasForeignKey("TenantId", "RegionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_question_banks_regions_tenant_id_region_id"); + }); + + modelBuilder.Entity("Tiku.Domain.QuestionBanks.QuestionVersion", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_question_versions_users_created_by"); + + b.HasOne("Tiku.Domain.QuestionBanks.Question", null) + .WithMany() + .HasForeignKey("TenantId", "QuestionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_question_versions_questions_tenant_id_question_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.Tenant", b => + { + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_tenants_users_owner_user_id"); + }); + + modelBuilder.Entity("Tiku.Domain.Tenancy.TenantMembership", b => + { + b.HasOne("Tiku.Domain.Tenancy.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_memberships_tenants_tenant_id"); + + b.HasOne("Tiku.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_memberships_users_user_id"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Tiku.Infrastructure/Persistence/ModelBuilderExtensions.cs b/Tiku.Infrastructure/Persistence/ModelBuilderExtensions.cs new file mode 100644 index 0000000..4910fb1 --- /dev/null +++ b/Tiku.Infrastructure/Persistence/ModelBuilderExtensions.cs @@ -0,0 +1,73 @@ +using System.Text; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Tiku.Infrastructure.Persistence; + +internal static class ModelBuilderExtensions +{ + public static void UseSnakeCaseIdentifiers(this ModelBuilder modelBuilder) + { + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + foreach (var property in entityType.GetProperties()) + { + property.SetColumnName(ToSnakeCase(property.Name)); + } + + foreach (var key in entityType.GetKeys()) + { + if (key.GetName() is { } keyName) + { + key.SetName(ToSnakeCase(keyName)); + } + } + + foreach (var foreignKey in entityType.GetForeignKeys()) + { + if (foreignKey.GetConstraintName() is { } constraintName) + { + foreignKey.SetConstraintName(ToSnakeCase(constraintName)); + } + } + + foreach (var index in entityType.GetIndexes()) + { + if (index.GetDatabaseName() is { } indexName) + { + index.SetDatabaseName(ToSnakeCase(indexName)); + } + } + } + } + + public static string ToSnakeCase(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return value; + } + + var builder = new StringBuilder(value.Length + 8); + for (var index = 0; index < value.Length; index++) + { + var character = value[index]; + if (char.IsUpper(character) && index > 0 && + (char.IsLower(value[index - 1]) || + index + 1 < value.Length && char.IsLower(value[index + 1]))) + { + builder.Append('_'); + } + + builder.Append(char.ToLowerInvariant(character)); + } + + return builder.ToString(); + } +} + +internal sealed class SnakeCaseEnumConverter() : ValueConverter( + value => ModelBuilderExtensions.ToSnakeCase(value.ToString()), + value => Enum.Parse(value.Replace("_", string.Empty), true)) + where TEnum : struct, Enum; diff --git a/Tiku.Infrastructure/Persistence/TikuDbContext.cs b/Tiku.Infrastructure/Persistence/TikuDbContext.cs new file mode 100644 index 0000000..c9281ce --- /dev/null +++ b/Tiku.Infrastructure/Persistence/TikuDbContext.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore; +using Tiku.Domain.Catalog; +using Tiku.Domain.Common; +using Tiku.Domain.Identity; +using Tiku.Domain.Learning; +using Tiku.Domain.QuestionBanks; +using Tiku.Domain.Tenancy; + +namespace Tiku.Infrastructure.Persistence; + +public sealed class TikuDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Tenants => Set(); + public DbSet Users => Set(); + public DbSet UserIdentities => Set(); + public DbSet TenantMemberships => Set(); + public DbSet StudentProfiles => Set(); + public DbSet Regions => Set(); + public DbSet RegionModules => Set(); + public DbSet ModuleNodes => Set(); + public DbSet Schools => Set(); + public DbSet Majors => Set(); + public DbSet Subjects => Set(); + public DbSet Categories => Set(); + public DbSet QuestionBanks => Set(); + public DbSet Questions => Set(); + public DbSet QuestionVersions => Set(); + public DbSet PracticeSessions => Set(); + public DbSet AnswerRecords => Set(); + public DbSet FavoriteQuestions => Set(); + public DbSet WrongQuestions => Set(); + public DbSet RecentPractices => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasPostgresExtension("citext"); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(TikuDbContext).Assembly); + modelBuilder.UseSnakeCaseIdentifiers(); + } + + public override int SaveChanges(bool acceptAllChangesOnSuccess) + { + UpdateTimestamps(); + return base.SaveChanges(acceptAllChangesOnSuccess); + } + + public override Task SaveChangesAsync( + bool acceptAllChangesOnSuccess, + CancellationToken cancellationToken = default) + { + UpdateTimestamps(); + return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + } + + private void UpdateTimestamps() + { + var now = DateTimeOffset.UtcNow; + + foreach (var entry in ChangeTracker.Entries()) + { + if (entry.State == EntityState.Added) + { + entry.Entity.CreatedAt = now; + } + + if (entry.State is EntityState.Added or EntityState.Modified) + { + entry.Entity.UpdatedAt = now; + } + } + } +} diff --git a/Tiku.Infrastructure/Tiku.Infrastructure.csproj b/Tiku.Infrastructure/Tiku.Infrastructure.csproj index fb9096f..d705899 100644 --- a/Tiku.Infrastructure/Tiku.Infrastructure.csproj +++ b/Tiku.Infrastructure/Tiku.Infrastructure.csproj @@ -5,6 +5,14 @@ + + + + + + + + net10.0 enable diff --git a/Tiku.IntegrationTests/PersistenceModelTests.cs b/Tiku.IntegrationTests/PersistenceModelTests.cs new file mode 100644 index 0000000..3208d5b --- /dev/null +++ b/Tiku.IntegrationTests/PersistenceModelTests.cs @@ -0,0 +1,68 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Tiku.Domain.QuestionBanks; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.IntegrationTests; + +public sealed class PersistenceModelTests +{ + private static readonly DbContextOptions Options = + new DbContextOptionsBuilder() + .UseNpgsql("Host=localhost;Database=tiku_model_tests;Username=postgres") + .Options; + + [Fact] + public void Core_model_contains_expected_tables() + { + using var context = new TikuDbContext(Options); + + var tableNames = context.Model.GetEntityTypes() + .Select(entity => entity.GetTableName()) + .ToHashSet(StringComparer.Ordinal); + + Assert.Equal(20, tableNames.Count); + Assert.Contains("tenants", tableNames); + Assert.Contains("questions", tableNames); + Assert.Contains("question_versions", tableNames); + Assert.Contains("answer_records", tableNames); + } + + [Theory] + [InlineData(nameof(Question.Tags), "'[]'::jsonb")] + public void Question_json_properties_are_mapped_to_jsonb( + string propertyName, + string expectedDefaultValueSql) + { + using var context = new TikuDbContext(Options); + + var property = context.Model.FindEntityType(typeof(Question))! + .FindProperty(propertyName)!; + + Assert.Equal("jsonb", property.GetColumnType()); + Assert.Equal(expectedDefaultValueSql, property.GetDefaultValueSql()); + Assert.Equal(typeof(System.Text.Json.JsonElement), property.ClrType); + } + + [Fact] + public void Question_relations_include_tenant_in_foreign_keys() + { + using var context = new TikuDbContext(Options); + + var foreignKeys = context.Model.FindEntityType(typeof(Question))! + .GetForeignKeys() + .Select(foreignKey => foreignKey.Properties + .Select(property => property.Name) + .ToArray()) + .ToArray(); + + Assert.All( + foreignKeys.Where(properties => properties.Length > 1), + properties => Assert.Contains("TenantId", properties)); + + Assert.Contains( + foreignKeys, + properties => properties.SequenceEqual( + ["TenantId", "Id", "CurrentVersionId"])); + } +} diff --git a/Tiku.IntegrationTests/UnitTest1.cs b/Tiku.IntegrationTests/UnitTest1.cs deleted file mode 100644 index 9a44487..0000000 --- a/Tiku.IntegrationTests/UnitTest1.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Tiku.IntegrationTests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - - } -} diff --git a/dotnet-tools.json b/dotnet-tools.json new file mode 100644 index 0000000..cce89d8 --- /dev/null +++ b/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "10.0.10", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file