forked from gongxuegit/tiku-backend.net
feat: enforce tenant isolation and shared question bank
This commit is contained in:
@@ -182,7 +182,7 @@ internal sealed class ContentImportItemConfiguration : IEntityTypeConfiguration<
|
||||
builder.Property(entity => entity.SourcePayload).IsJson("{}");
|
||||
builder.Property(entity => entity.NormalizedPayload).IsJson("{}");
|
||||
builder.Property(entity => entity.ContentHash).HasMaxLength(128);
|
||||
builder.HasIndex(entity => new { entity.JobId, entity.RowNo }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.JobId, entity.RowNo }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.JobId, entity.Status, entity.RowNo });
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
|
||||
@@ -134,7 +134,7 @@ internal sealed class PaymentConfiguration : IEntityTypeConfiguration<Payment>
|
||||
builder.Property(entity => entity.RawPayload).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.OrderId, entity.UpdatedAt });
|
||||
builder.HasIndex(entity => new { entity.Provider, entity.ProviderTradeNo })
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Provider, entity.ProviderTradeNo })
|
||||
.IsUnique()
|
||||
.HasFilter("provider_trade_no is not null");
|
||||
builder.ToTable(table =>
|
||||
@@ -160,7 +160,7 @@ internal sealed class PaymentEventConfiguration : IEntityTypeConfiguration<Payme
|
||||
builder.Property(entity => entity.EventId).HasMaxLength(200);
|
||||
builder.Property(entity => entity.Payload).IsJson("{}");
|
||||
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
||||
builder.HasIndex(entity => new { entity.Provider, entity.EventId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Provider, entity.EventId }).IsUnique();
|
||||
builder.HasOne<Tenant>().WithMany().HasForeignKey(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<Payment>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.PaymentId })
|
||||
@@ -438,7 +438,7 @@ internal sealed class CommerceReconciliationItemConfiguration : IEntityTypeConfi
|
||||
builder.Property(entity => entity.IssueCode).HasMaxLength(100);
|
||||
builder.Property(entity => entity.Details).IsJson("{}");
|
||||
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
||||
builder.HasIndex(entity => new { entity.BatchId, entity.RowNo }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.BatchId, entity.RowNo }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.BatchId, entity.MatchStatus, entity.RowNo });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.OrderNo, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.RefundNo, entity.ProviderRefundNo, entity.CreatedAt });
|
||||
|
||||
@@ -180,7 +180,7 @@ internal sealed class QuestionCollectionItemConfiguration :
|
||||
{
|
||||
entity.TenantId,
|
||||
entity.CollectionId,
|
||||
entity.QuestionId
|
||||
entity.QuestionReferenceId
|
||||
}).IsUnique();
|
||||
builder.HasIndex(entity => new
|
||||
{
|
||||
@@ -194,10 +194,14 @@ internal sealed class QuestionCollectionItemConfiguration :
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.CollectionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<Question>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionId })
|
||||
builder.HasOne<TenantQuestionReference>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionReferenceId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<Question>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.QuestionOwnerTenantId, entity.QuestionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,72 +65,48 @@ internal sealed class ContentAssetSecurityScanEventConfiguration : IEntityTypeCo
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class QuestionBankGrantConfiguration : IEntityTypeConfiguration<QuestionBankGrant>
|
||||
internal sealed class TenantQuestionBankPreferenceConfiguration : IEntityTypeConfiguration<TenantQuestionBankPreference>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<QuestionBankGrant> builder)
|
||||
public void Configure(EntityTypeBuilder<TenantQuestionBankPreference> builder)
|
||||
{
|
||||
builder.ConfigureEntity("question_bank_grants");
|
||||
builder.ConfigureTenantEntity("tenant_question_bank_preferences");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(entity => entity.GrantScope).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.AllowedPlanCodes)
|
||||
.HasColumnType("text[]")
|
||||
.HasDefaultValueSql("'{}'::text[]");
|
||||
builder.Property(entity => entity.AllowedTenantIds)
|
||||
.HasColumnType("uuid[]")
|
||||
.HasDefaultValueSql("'{}'::uuid[]");
|
||||
builder.Property(entity => entity.AllowedRegionIds)
|
||||
.HasColumnType("uuid[]")
|
||||
.HasDefaultValueSql("'{}'::uuid[]");
|
||||
builder.Property(entity => entity.AllowedSubjectIds)
|
||||
.HasColumnType("uuid[]")
|
||||
.HasDefaultValueSql("'{}'::uuid[]");
|
||||
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Alias).HasMaxLength(300);
|
||||
builder.Property(entity => entity.NavigationLocation).HasMaxLength(100);
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.SourceQuestionBankId, entity.Status, entity.StartsAt, entity.ExpiresAt });
|
||||
builder.HasIndex(entity => entity.AllowedTenantIds).HasMethod("gin");
|
||||
builder.HasIndex(entity => entity.AllowedPlanCodes).HasMethod("gin");
|
||||
builder.HasIndex(entity => new
|
||||
{
|
||||
entity.TenantId,
|
||||
entity.QuestionBankOwnerTenantId,
|
||||
entity.QuestionBankId
|
||||
}).IsUnique();
|
||||
builder.HasOne<QuestionBank>().WithMany()
|
||||
.HasForeignKey(entity => entity.SourceQuestionBankId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.HasForeignKey(entity => new { entity.QuestionBankOwnerTenantId, entity.QuestionBankId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.CreatedBy).OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.UpdatedBy).OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TenantQuestionBankAdoptionConfiguration : IEntityTypeConfiguration<TenantQuestionBankAdoption>
|
||||
internal sealed class TenantQuestionReferenceConfiguration : IEntityTypeConfiguration<TenantQuestionReference>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TenantQuestionBankAdoption> builder)
|
||||
public void Configure(EntityTypeBuilder<TenantQuestionReference> builder)
|
||||
{
|
||||
builder.ConfigureTenantEntity("tenant_question_bank_adoptions");
|
||||
builder.ConfigureTenantEntity("tenant_question_references");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(entity => entity.AdoptionMode).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.SyncStatus).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.SourceSnapshot).IsJson("{}");
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.SourceQuestionBankId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.UpdatedAt });
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_tenant_question_bank_adoptions_copied_count", "copied_question_count >= 0"));
|
||||
builder.HasOne<QuestionBank>().WithMany()
|
||||
.HasForeignKey(entity => entity.SourceQuestionBankId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<QuestionBankGrant>().WithMany()
|
||||
.HasForeignKey(entity => entity.GrantId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<QuestionBank>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.TargetQuestionBankId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<ContentEntry>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.TargetEntryId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<QuestionCollection>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.TargetCollectionId })
|
||||
builder.Property(entity => entity.Source).HasSnakeCaseEnum();
|
||||
builder.HasAlternateKey(entity => new
|
||||
{
|
||||
entity.TenantId,
|
||||
entity.QuestionOwnerTenantId,
|
||||
entity.QuestionId
|
||||
});
|
||||
builder.HasOne<Question>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.QuestionOwnerTenantId, entity.QuestionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.CreatedBy).OnDelete(DeleteBehavior.SetNull);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.UpdatedBy).OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ internal sealed class PracticeSessionConfiguration : IEntityTypeConfiguration<Pr
|
||||
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.QuestionIds).IsJson("[]");
|
||||
builder.Property(entity => entity.TotalScore).HasPrecision(8, 2);
|
||||
builder.Property(entity => entity.AccessMode)
|
||||
.HasSnakeCaseEnum()
|
||||
@@ -55,6 +54,45 @@ internal sealed class PracticeSessionConfiguration : IEntityTypeConfiguration<Pr
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PracticeSessionQuestionConfiguration : IEntityTypeConfiguration<PracticeSessionQuestion>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PracticeSessionQuestion> builder)
|
||||
{
|
||||
builder.ConfigureTenantEntity("practice_session_questions");
|
||||
builder.HasAlternateKey(entity => new { entity.TenantId, entity.PracticeSessionId, entity.Id });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.PracticeSessionId, entity.Position }).IsUnique();
|
||||
builder.Property(entity => entity.Score).HasPrecision(8, 2);
|
||||
|
||||
builder.HasOne<PracticeSession>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.PracticeSessionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<TenantQuestionReference>().WithMany()
|
||||
.HasForeignKey(entity => new
|
||||
{
|
||||
entity.TenantId,
|
||||
entity.QuestionOwnerTenantId,
|
||||
entity.QuestionId
|
||||
})
|
||||
.HasPrincipalKey(entity => new
|
||||
{
|
||||
entity.TenantId,
|
||||
entity.QuestionOwnerTenantId,
|
||||
entity.QuestionId
|
||||
})
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<QuestionVersion>().WithMany()
|
||||
.HasForeignKey(entity => new
|
||||
{
|
||||
TenantId = entity.QuestionOwnerTenantId,
|
||||
entity.QuestionId,
|
||||
Id = entity.QuestionVersionId
|
||||
})
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration<AnswerRecord>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AnswerRecord> builder)
|
||||
@@ -67,13 +105,6 @@ internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration<Answe
|
||||
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,
|
||||
@@ -81,31 +112,9 @@ internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration<Answe
|
||||
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<User>().WithMany()
|
||||
.HasForeignKey(entity => entity.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<Question>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<QuestionVersion>().WithMany()
|
||||
.HasForeignKey(entity => new
|
||||
{
|
||||
entity.TenantId,
|
||||
entity.QuestionId,
|
||||
entity.QuestionVersionId
|
||||
})
|
||||
.HasPrincipalKey(entity => new
|
||||
{
|
||||
entity.TenantId,
|
||||
entity.QuestionId,
|
||||
entity.Id
|
||||
})
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<PracticeSession>().WithMany()
|
||||
.HasForeignKey(entity => new
|
||||
{
|
||||
@@ -120,6 +129,20 @@ internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration<Answe
|
||||
entity.Id
|
||||
})
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<PracticeSessionQuestion>().WithMany()
|
||||
.HasForeignKey(entity => new
|
||||
{
|
||||
entity.TenantId,
|
||||
entity.PracticeSessionId,
|
||||
Id = entity.SessionQuestionId
|
||||
})
|
||||
.HasPrincipalKey(entity => new
|
||||
{
|
||||
entity.TenantId,
|
||||
entity.PracticeSessionId,
|
||||
entity.Id
|
||||
})
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +151,7 @@ internal sealed class FavoriteQuestionConfiguration : IEntityTypeConfiguration<F
|
||||
public void Configure(EntityTypeBuilder<FavoriteQuestion> builder)
|
||||
{
|
||||
builder.ToTable("favorite_questions");
|
||||
builder.HasKey(entity => new { entity.TenantId, entity.UserId, entity.QuestionId });
|
||||
builder.HasKey(entity => new { entity.TenantId, entity.UserId, entity.QuestionReferenceId });
|
||||
builder.Property(entity => entity.Source).HasMaxLength(50);
|
||||
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
||||
|
||||
@@ -138,10 +161,14 @@ internal sealed class FavoriteQuestionConfiguration : IEntityTypeConfiguration<F
|
||||
builder.HasOne<User>().WithMany()
|
||||
.HasForeignKey(entity => entity.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<Question>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionId })
|
||||
builder.HasOne<TenantQuestionReference>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionReferenceId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<Question>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.QuestionOwnerTenantId, entity.QuestionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +177,7 @@ internal sealed class WrongQuestionConfiguration : IEntityTypeConfiguration<Wron
|
||||
public void Configure(EntityTypeBuilder<WrongQuestion> builder)
|
||||
{
|
||||
builder.ToTable("wrong_questions");
|
||||
builder.HasKey(entity => new { entity.TenantId, entity.UserId, entity.QuestionId });
|
||||
builder.HasKey(entity => new { entity.TenantId, entity.UserId, entity.QuestionReferenceId });
|
||||
builder.Property(entity => entity.WrongCount).HasDefaultValue(1);
|
||||
builder.Property(entity => entity.LastWrongAt).HasDefaultValueSql("now()");
|
||||
|
||||
@@ -160,10 +187,14 @@ internal sealed class WrongQuestionConfiguration : IEntityTypeConfiguration<Wron
|
||||
builder.HasOne<User>().WithMany()
|
||||
.HasForeignKey(entity => entity.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<Question>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionId })
|
||||
builder.HasOne<TenantQuestionReference>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionReferenceId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<Question>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.QuestionOwnerTenantId, entity.QuestionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ internal sealed class TenantInvoiceConfiguration : IEntityTypeConfiguration<Tena
|
||||
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Currency).HasMaxLength(10);
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => entity.InvoiceNo).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.InvoiceNo }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.DueDate });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.BillingPeriodStart, entity.BillingPeriodEnd })
|
||||
.IsUnique()
|
||||
@@ -110,7 +110,7 @@ internal sealed class TenantInvoicePaymentConfiguration : IEntityTypeConfigurati
|
||||
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.ProviderTradeNo).HasMaxLength(200);
|
||||
builder.Property(entity => entity.RawPayload).IsJson("{}");
|
||||
builder.HasIndex(entity => entity.PaymentNo).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.PaymentNo }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.InvoiceId, entity.Status });
|
||||
builder.ToTable(table => table.HasCheckConstraint("ck_tenant_invoice_payments_amount", "amount_cents >= 0"));
|
||||
builder.HasOne<TenantInvoice>().WithMany()
|
||||
@@ -229,7 +229,7 @@ internal sealed class PlatformDunningNotificationEventConfiguration : IEntityTyp
|
||||
builder.Property(entity => entity.RequestPayload).IsJson("{}");
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.Property(entity => entity.ScheduledAt).HasDefaultValueSql("now()");
|
||||
builder.HasIndex(entity => new { entity.ChannelId, entity.ReminderId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.ChannelId, entity.ReminderId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.Status, entity.NextAttemptAt, entity.ScheduledAt, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.ReminderId, entity.Status, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.InvoiceId, entity.Status, entity.CreatedAt });
|
||||
|
||||
@@ -14,7 +14,6 @@ internal sealed class QuestionBankConfiguration : IEntityTypeConfiguration<Quest
|
||||
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("{}");
|
||||
|
||||
@@ -94,7 +93,7 @@ internal sealed class QuestionVersionConfiguration : IEntityTypeConfiguration<Qu
|
||||
builder.Property(entity => entity.SourceHash).HasMaxLength(128);
|
||||
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
||||
|
||||
builder.HasIndex(entity => new { entity.QuestionId, entity.VersionNo }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.QuestionId, entity.VersionNo }).IsUnique();
|
||||
builder.HasOne<Question>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence.Configurations;
|
||||
|
||||
internal sealed class TaxonomyNodeConfiguration : IEntityTypeConfiguration<TaxonomyNode>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TaxonomyNode> builder)
|
||||
{
|
||||
builder.ConfigureTenantEntity("taxonomy_nodes");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(entity => entity.NodeType).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Code).HasMaxLength(100);
|
||||
builder.Property(entity => entity.Name).HasMaxLength(300);
|
||||
builder.Property(entity => entity.Path).HasColumnType("ltree");
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Code }).IsUnique();
|
||||
builder.HasOne<TaxonomyNode>().WithMany()
|
||||
.HasForeignKey(entity => new { TenantId = entity.ParentOwnerTenantId, Id = entity.ParentId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.ToTable(table => table.HasCheckConstraint(
|
||||
"ck_taxonomy_nodes_parent_pair",
|
||||
"(parent_owner_tenant_id is null) = (parent_id is null)"));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class QuestionTaxonomyAssignmentConfiguration : IEntityTypeConfiguration<QuestionTaxonomyAssignment>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<QuestionTaxonomyAssignment> builder)
|
||||
{
|
||||
builder.ConfigureEntity("question_taxonomy_assignments");
|
||||
builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id });
|
||||
builder.HasIndex(entity => new
|
||||
{
|
||||
entity.TenantId,
|
||||
entity.QuestionId,
|
||||
entity.TaxonomyOwnerTenantId,
|
||||
entity.TaxonomyNodeId
|
||||
}).IsUnique();
|
||||
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
||||
builder.HasOne<Question>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<TaxonomyNode>().WithMany()
|
||||
.HasForeignKey(entity => new { TenantId = entity.TaxonomyOwnerTenantId, Id = entity.TaxonomyNodeId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,9 @@ internal sealed class TenantConfiguration : IEntityTypeConfiguration<Tenant>
|
||||
|
||||
builder.HasIndex(entity => entity.Slug).IsUnique();
|
||||
builder.HasIndex(entity => entity.LegacyId).IsUnique();
|
||||
builder.HasIndex(entity => entity.Mode)
|
||||
.IsUnique()
|
||||
.HasFilter("mode = 'platform_owned'");
|
||||
|
||||
builder.HasOne<User>()
|
||||
.WithMany()
|
||||
@@ -67,7 +70,10 @@ internal sealed class TenantDomainConfiguration : IEntityTypeConfiguration<Tenan
|
||||
builder.Property(entity => entity.DomainType).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.VerificationToken).HasMaxLength(256);
|
||||
builder.HasIndex(entity => entity.Host).IsUnique();
|
||||
builder.Property(entity => entity.LastFailureReason).HasMaxLength(2000);
|
||||
builder.HasIndex(entity => entity.Host)
|
||||
.IsUnique()
|
||||
.HasAnnotation("Tiku:GlobalUnique", true);
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.IsPrimary })
|
||||
.IsUnique()
|
||||
.HasFilter("is_primary");
|
||||
@@ -114,3 +120,29 @@ internal sealed class TenantSettingsConfiguration : IEntityTypeConfiguration<Ten
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TenantFrontendConfigConfiguration : IEntityTypeConfiguration<TenantFrontendConfig>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TenantFrontendConfig> builder)
|
||||
{
|
||||
builder.ConfigureTenantEntity("tenant_frontend_configs");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.HasIndex(entity => entity.TenantId).IsUnique();
|
||||
builder.Property(entity => entity.ConfigVersion).IsConcurrencyToken();
|
||||
builder.Property(entity => entity.PublishedBranding).IsJson("{}");
|
||||
builder.Property(entity => entity.PublishedTheme).IsJson("{}");
|
||||
builder.Property(entity => entity.PublishedFeatures).IsJson("{}");
|
||||
builder.Property(entity => entity.PublishedNavigation).IsJson("[]");
|
||||
builder.Property(entity => entity.PublishedHomeModules).IsJson("[]");
|
||||
builder.Property(entity => entity.DraftBranding).IsJson("{}");
|
||||
builder.Property(entity => entity.DraftTheme).IsJson("{}");
|
||||
builder.Property(entity => entity.DraftFeatures).IsJson("{}");
|
||||
builder.Property(entity => entity.DraftNavigation).IsJson("[]");
|
||||
builder.Property(entity => entity.DraftHomeModules).IsJson("[]");
|
||||
builder.ToTable(table =>
|
||||
{
|
||||
table.HasCheckConstraint("ck_tenant_frontend_configs_schema_version", "schema_version > 0");
|
||||
table.HasCheckConstraint("ck_tenant_frontend_configs_config_version", "config_version > 0");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,9 @@ internal sealed class AuthSessionConfiguration : IEntityTypeConfiguration<AuthSe
|
||||
builder.Property(entity => entity.IpAddress).HasMaxLength(64);
|
||||
builder.Property(entity => entity.UserAgent).HasMaxLength(1000);
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => entity.TokenHash).IsUnique();
|
||||
builder.HasIndex(entity => entity.TokenHash)
|
||||
.IsUnique()
|
||||
.HasAnnotation("Tiku:GlobalUnique", true);
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.ExpiresAt })
|
||||
.HasFilter("revoked_at is null");
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,47 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddProductActiveAndOperationsCatalog : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_products_tenant_id_region_id_type_sort_order",
|
||||
table: "products");
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "is_active",
|
||||
table: "products",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_products_tenant_id_region_id_type_is_active_sort_order",
|
||||
table: "products",
|
||||
columns: new[] { "tenant_id", "region_id", "type", "is_active", "sort_order" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_products_tenant_id_region_id_type_is_active_sort_order",
|
||||
table: "products");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "is_active",
|
||||
table: "products");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_products_tenant_id_region_id_type_sort_order",
|
||||
table: "products",
|
||||
columns: new[] { "tenant_id", "region_id", "type", "sort_order" });
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,157 +0,0 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddScorelineDynamicRecords : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "scoreline_fields",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
region_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
legacy_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
field_key = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
field_name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
field_type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
unit = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||
is_filter = table.Column<bool>(type: "boolean", nullable: false),
|
||||
is_required = table.Column<bool>(type: "boolean", nullable: false),
|
||||
is_visible = table.Column<bool>(type: "boolean", nullable: false),
|
||||
is_trend = table.Column<bool>(type: "boolean", nullable: false),
|
||||
options = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'[]'::jsonb"),
|
||||
placeholder = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||
sort_order = table.Column<int>(type: "integer", nullable: false),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_scoreline_fields", x => x.id);
|
||||
table.UniqueConstraint("ak_scoreline_fields_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.ForeignKey(
|
||||
name: "fk_scoreline_fields_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_scoreline_fields_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "scoreline_records",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
region_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
school_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
major_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
legacy_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
year = table.Column<int>(type: "integer", nullable: false),
|
||||
school_name = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: true),
|
||||
major_name = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: true),
|
||||
field_values = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_scoreline_records", x => x.id);
|
||||
table.UniqueConstraint("ak_scoreline_records_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.ForeignKey(
|
||||
name: "fk_scoreline_records_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_scoreline_records_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_scoreline_records_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_scoreline_records_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_scoreline_fields_tenant_id_legacy_id",
|
||||
table: "scoreline_fields",
|
||||
columns: new[] { "tenant_id", "legacy_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_scoreline_fields_tenant_id_region_id_field_key",
|
||||
table: "scoreline_fields",
|
||||
columns: new[] { "tenant_id", "region_id", "field_key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_scoreline_fields_tenant_id_region_id_is_filter_sort_order",
|
||||
table: "scoreline_fields",
|
||||
columns: new[] { "tenant_id", "region_id", "is_filter", "sort_order" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_scoreline_records_tenant_id_legacy_id",
|
||||
table: "scoreline_records",
|
||||
columns: new[] { "tenant_id", "legacy_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_scoreline_records_tenant_id_major_id",
|
||||
table: "scoreline_records",
|
||||
columns: new[] { "tenant_id", "major_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_scoreline_records_tenant_id_region_id_school_id_major_id_ye~",
|
||||
table: "scoreline_records",
|
||||
columns: new[] { "tenant_id", "region_id", "school_id", "major_id", "year" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_scoreline_records_tenant_id_school_id",
|
||||
table: "scoreline_records",
|
||||
columns: new[] { "tenant_id", "school_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_scoreline_records_tenant_id_year",
|
||||
table: "scoreline_records",
|
||||
columns: new[] { "tenant_id", "year" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "scoreline_fields");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "scoreline_records");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddStudentProfileAvatarPreset : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "avatar_preset",
|
||||
table: "student_profiles",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: false,
|
||||
defaultValue: "male");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "avatar_preset",
|
||||
table: "student_profiles");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,69 +0,0 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPaymentSdkAndTenantSecretFoundation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tenant_secrets",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
purpose = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
|
||||
provider = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
secret_key = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
|
||||
secret_ref = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: false),
|
||||
status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
secret_payload = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
rotated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
expires_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_tenant_secrets", x => x.id);
|
||||
table.UniqueConstraint("ak_tenant_secrets_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.ForeignKey(
|
||||
name: "fk_tenant_secrets_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tenant_secrets_tenant_id_purpose_provider_secret_key",
|
||||
table: "tenant_secrets",
|
||||
columns: new[] { "tenant_id", "purpose", "provider", "secret_key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tenant_secrets_tenant_id_purpose_provider_status",
|
||||
table: "tenant_secrets",
|
||||
columns: new[] { "tenant_id", "purpose", "provider", "status" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tenant_secrets_tenant_id_secret_ref",
|
||||
table: "tenant_secrets",
|
||||
columns: new[] { "tenant_id", "secret_ref" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "tenant_secrets");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,265 +0,0 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPointsPersistenceFoundation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "point_activity_tasks",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
task_key = table.Column<string>(type: "citext", maxLength: 100, nullable: false),
|
||||
title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||
task_type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
points = table.Column<int>(type: "integer", nullable: false),
|
||||
max_claims_per_user = table.Column<int>(type: "integer", nullable: false),
|
||||
starts_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
ends_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
sort_order = table.Column<int>(type: "integer", nullable: false),
|
||||
rules = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
metadata = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_point_activity_tasks", x => x.id);
|
||||
table.UniqueConstraint("ak_point_activity_tasks_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.CheckConstraint("ck_point_activity_tasks_max_claims", "max_claims_per_user > 0");
|
||||
table.CheckConstraint("ck_point_activity_tasks_points", "points > 0");
|
||||
table.ForeignKey(
|
||||
name: "fk_point_activity_tasks_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "point_exchange_items",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
region_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
item_key = table.Column<string>(type: "citext", maxLength: 100, nullable: false),
|
||||
name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||
item_type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
points_cost = table.Column<int>(type: "integer", nullable: false),
|
||||
stock = table.Column<int>(type: "integer", nullable: true),
|
||||
days = table.Column<int>(type: "integer", nullable: true),
|
||||
sort_order = table.Column<int>(type: "integer", nullable: false),
|
||||
starts_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
ends_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
fulfillment_payload = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
metadata = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_point_exchange_items", x => x.id);
|
||||
table.UniqueConstraint("ak_point_exchange_items_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.CheckConstraint("ck_point_exchange_items_days", "days is null or days >= 0");
|
||||
table.CheckConstraint("ck_point_exchange_items_points_cost", "points_cost > 0");
|
||||
table.CheckConstraint("ck_point_exchange_items_stock", "stock is null or stock >= 0");
|
||||
table.ForeignKey(
|
||||
name: "fk_point_exchange_items_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_point_exchange_items_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "point_activity_claims",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
task_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
user_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
task_key = table.Column<string>(type: "citext", maxLength: 100, nullable: false),
|
||||
points = table.Column<int>(type: "integer", nullable: false),
|
||||
status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
source_type = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
source_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
claimed_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
metadata = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_point_activity_claims", x => x.id);
|
||||
table.UniqueConstraint("ak_point_activity_claims_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.CheckConstraint("ck_point_activity_claims_points", "points > 0");
|
||||
table.ForeignKey(
|
||||
name: "fk_point_activity_claims_point_activity_tasks_tenant_id_task_id",
|
||||
columns: x => new { x.tenant_id, x.task_id },
|
||||
principalTable: "point_activity_tasks",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "fk_point_activity_claims_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_point_activity_claims_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "point_exchange_orders",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
item_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
user_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
order_no = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
item_name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
item_type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
points_cost = table.Column<int>(type: "integer", nullable: false),
|
||||
ordered_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
completed_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
cancelled_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
fulfillment_snapshot = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
metadata = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_point_exchange_orders", x => x.id);
|
||||
table.UniqueConstraint("ak_point_exchange_orders_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.CheckConstraint("ck_point_exchange_orders_points_cost", "points_cost > 0");
|
||||
table.ForeignKey(
|
||||
name: "fk_point_exchange_orders_point_exchange_items_tenant_id_item_id",
|
||||
columns: x => new { x.tenant_id, x.item_id },
|
||||
principalTable: "point_exchange_items",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "fk_point_exchange_orders_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_point_exchange_orders_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_point_activity_claims_tenant_id_task_id",
|
||||
table: "point_activity_claims",
|
||||
columns: new[] { "tenant_id", "task_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_point_activity_claims_tenant_id_user_id_created_at",
|
||||
table: "point_activity_claims",
|
||||
columns: new[] { "tenant_id", "user_id", "created_at" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_point_activity_claims_tenant_id_user_id_task_id_source_type~",
|
||||
table: "point_activity_claims",
|
||||
columns: new[] { "tenant_id", "user_id", "task_id", "source_type", "source_id" },
|
||||
unique: true,
|
||||
filter: "source_type is not null and source_id is not null");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_point_activity_claims_user_id",
|
||||
table: "point_activity_claims",
|
||||
column: "user_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_point_activity_tasks_tenant_id_status_sort_order",
|
||||
table: "point_activity_tasks",
|
||||
columns: new[] { "tenant_id", "status", "sort_order" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_point_activity_tasks_tenant_id_task_key",
|
||||
table: "point_activity_tasks",
|
||||
columns: new[] { "tenant_id", "task_key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_point_exchange_items_tenant_id_item_key",
|
||||
table: "point_exchange_items",
|
||||
columns: new[] { "tenant_id", "item_key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_point_exchange_items_tenant_id_region_id_status_sort_order",
|
||||
table: "point_exchange_items",
|
||||
columns: new[] { "tenant_id", "region_id", "status", "sort_order" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_point_exchange_orders_tenant_id_item_id",
|
||||
table: "point_exchange_orders",
|
||||
columns: new[] { "tenant_id", "item_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_point_exchange_orders_tenant_id_order_no",
|
||||
table: "point_exchange_orders",
|
||||
columns: new[] { "tenant_id", "order_no" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_point_exchange_orders_tenant_id_user_id_created_at",
|
||||
table: "point_exchange_orders",
|
||||
columns: new[] { "tenant_id", "user_id", "created_at" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_point_exchange_orders_user_id",
|
||||
table: "point_exchange_orders",
|
||||
column: "user_id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "point_activity_claims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "point_exchange_orders");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "point_activity_tasks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "point_exchange_items");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,157 +0,0 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCommissionSettlementProofs : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "commission_settlement_export_events",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
settlement_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
exported_by = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
export_format = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
filename = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
row_count = table.Column<int>(type: "integer", nullable: false),
|
||||
content_sha256 = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||
metadata = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_commission_settlement_export_events", x => x.id);
|
||||
table.CheckConstraint("ck_commission_export_events_rows", "row_count >= 0");
|
||||
table.ForeignKey(
|
||||
name: "fk_commission_settlement_export_events_commission_settlements_~",
|
||||
columns: x => new { x.tenant_id, x.settlement_id },
|
||||
principalTable: "commission_settlements",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_commission_settlement_export_events_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_commission_settlement_export_events_users_exported_by",
|
||||
column: x => x.exported_by,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "commission_settlement_proofs",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
settlement_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
asset_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
submitted_by = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
reviewed_by = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
proof_type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
description = table.Column<string>(type: "text", nullable: true),
|
||||
external_url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
amount_cents = table.Column<int>(type: "integer", nullable: true),
|
||||
payment_method = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
payment_account = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
paid_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
reviewed_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
review_note = table.Column<string>(type: "text", nullable: true),
|
||||
metadata = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_commission_settlement_proofs", x => x.id);
|
||||
table.UniqueConstraint("ak_commission_settlement_proofs_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.CheckConstraint("ck_commission_settlement_proofs_amount", "amount_cents is null or amount_cents >= 0");
|
||||
table.ForeignKey(
|
||||
name: "fk_commission_settlement_proofs_commission_settlements_tenant_~",
|
||||
columns: x => new { x.tenant_id, x.settlement_id },
|
||||
principalTable: "commission_settlements",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_commission_settlement_proofs_content_assets_tenant_id_asset~",
|
||||
columns: x => new { x.tenant_id, x.asset_id },
|
||||
principalTable: "content_assets",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "fk_commission_settlement_proofs_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_commission_settlement_proofs_users_reviewed_by",
|
||||
column: x => x.reviewed_by,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "fk_commission_settlement_proofs_users_submitted_by",
|
||||
column: x => x.submitted_by,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_commission_settlement_export_events_exported_by",
|
||||
table: "commission_settlement_export_events",
|
||||
column: "exported_by");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_commission_settlement_export_events_tenant_id_settlement_id~",
|
||||
table: "commission_settlement_export_events",
|
||||
columns: new[] { "tenant_id", "settlement_id", "created_at" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_commission_settlement_proofs_reviewed_by",
|
||||
table: "commission_settlement_proofs",
|
||||
column: "reviewed_by");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_commission_settlement_proofs_submitted_by",
|
||||
table: "commission_settlement_proofs",
|
||||
column: "submitted_by");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_commission_settlement_proofs_tenant_id_asset_id",
|
||||
table: "commission_settlement_proofs",
|
||||
columns: new[] { "tenant_id", "asset_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_commission_settlement_proofs_tenant_id_settlement_id_status~",
|
||||
table: "commission_settlement_proofs",
|
||||
columns: new[] { "tenant_id", "settlement_id", "status", "created_at" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "commission_settlement_export_events");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "commission_settlement_proofs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class EncryptTenantSecretPayloads : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "secret_payload",
|
||||
table: "tenant_secrets");
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "encrypted_payload",
|
||||
table: "tenant_secrets",
|
||||
type: "bytea",
|
||||
nullable: false,
|
||||
defaultValue: new byte[0]);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "encryption_key_id",
|
||||
table: "tenant_secrets",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "encryption_nonce",
|
||||
table: "tenant_secrets",
|
||||
type: "bytea",
|
||||
nullable: false,
|
||||
defaultValue: new byte[0]);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "encryption_tag",
|
||||
table: "tenant_secrets",
|
||||
type: "bytea",
|
||||
nullable: false,
|
||||
defaultValue: new byte[0]);
|
||||
|
||||
migrationBuilder.AddCheckConstraint(
|
||||
name: "ck_tenant_secrets_encryption_envelope",
|
||||
table: "tenant_secrets",
|
||||
sql: "octet_length(encrypted_payload) > 0 and octet_length(encryption_nonce) = 12 and octet_length(encryption_tag) = 16 and encryption_key_id <> ''");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropCheckConstraint(
|
||||
name: "ck_tenant_secrets_encryption_envelope",
|
||||
table: "tenant_secrets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "encrypted_payload",
|
||||
table: "tenant_secrets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "encryption_key_id",
|
||||
table: "tenant_secrets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "encryption_nonce",
|
||||
table: "tenant_secrets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "encryption_tag",
|
||||
table: "tenant_secrets");
|
||||
|
||||
migrationBuilder.AddColumn<JsonElement>(
|
||||
name: "secret_payload",
|
||||
table: "tenant_secrets",
|
||||
type: "jsonb",
|
||||
nullable: false,
|
||||
defaultValueSql: "'{}'::jsonb");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence;
|
||||
|
||||
public sealed class TenantIsolationSaveChangesInterceptor(ITenantContext tenantContext) : SaveChangesInterceptor
|
||||
{
|
||||
public override InterceptionResult<int> SavingChanges(
|
||||
DbContextEventData eventData,
|
||||
InterceptionResult<int> result)
|
||||
{
|
||||
Enforce(eventData.Context);
|
||||
return result;
|
||||
}
|
||||
|
||||
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
|
||||
DbContextEventData eventData,
|
||||
InterceptionResult<int> result,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Enforce(eventData.Context);
|
||||
return ValueTask.FromResult(result);
|
||||
}
|
||||
|
||||
private void Enforce(DbContext? dbContext)
|
||||
{
|
||||
if (dbContext is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var entry in dbContext.ChangeTracker.Entries()
|
||||
.Where(entry => entry.State is EntityState.Added or EntityState.Modified or EntityState.Deleted))
|
||||
{
|
||||
var tenantProperty = entry.Metadata.FindProperty("TenantId");
|
||||
if (tenantProperty?.ClrType != typeof(Guid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var property = entry.Property("TenantId");
|
||||
var currentTenantId = (Guid)(property.CurrentValue ?? Guid.Empty);
|
||||
var originalTenantId = (Guid)(property.OriginalValue ?? Guid.Empty);
|
||||
if (entry.State == EntityState.Added)
|
||||
{
|
||||
if (!tenantContext.TenantId.HasValue && !tenantContext.IsSystem)
|
||||
{
|
||||
throw new TenantIsolationException(
|
||||
entry.Metadata.ClrType,
|
||||
"Cannot add tenant-owned data without a resolved tenant.");
|
||||
}
|
||||
|
||||
if (currentTenantId == Guid.Empty && tenantContext.TenantId.HasValue)
|
||||
{
|
||||
property.CurrentValue = tenantContext.TenantId.Value;
|
||||
currentTenantId = tenantContext.TenantId.Value;
|
||||
}
|
||||
|
||||
if (!tenantContext.IsSystem && currentTenantId != tenantContext.TenantId)
|
||||
{
|
||||
throw new TenantIsolationException(
|
||||
entry.Metadata.ClrType,
|
||||
"Cannot add data for another tenant.");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (property.IsModified || currentTenantId != originalTenantId)
|
||||
{
|
||||
throw new TenantIsolationException(
|
||||
entry.Metadata.ClrType,
|
||||
"Tenant ownership cannot be changed.");
|
||||
}
|
||||
|
||||
if (!tenantContext.IsSystem &&
|
||||
(!tenantContext.TenantId.HasValue || originalTenantId != tenantContext.TenantId.Value))
|
||||
{
|
||||
throw new TenantIsolationException(
|
||||
entry.Metadata.ClrType,
|
||||
"Cannot modify or delete data owned by another tenant.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TenantIsolationException(Type entityType, string message)
|
||||
: InvalidOperationException($"Tenant isolation rejected {entityType.Name}: {message}");
|
||||
@@ -1,4 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Reflection;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Common;
|
||||
@@ -14,8 +16,26 @@ using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence;
|
||||
|
||||
public sealed class TikuDbContext(DbContextOptions<TikuDbContext> options) : DbContext(options)
|
||||
public sealed class TikuDbContext(
|
||||
DbContextOptions<TikuDbContext> options,
|
||||
ITenantContext tenantContext) : DbContext(options)
|
||||
{
|
||||
public TikuDbContext(DbContextOptions<TikuDbContext> options)
|
||||
: this(options, CreateToolingTenantContext())
|
||||
{
|
||||
}
|
||||
|
||||
public Guid? CurrentTenantId => tenantContext.TenantId;
|
||||
public Guid CurrentTenantIdOrEmpty => tenantContext.TenantId ?? Guid.Empty;
|
||||
public bool IsTenantResolved => tenantContext.IsResolved;
|
||||
public bool IsSystemScope => tenantContext.IsSystem;
|
||||
|
||||
private static ITenantContext CreateToolingTenantContext()
|
||||
{
|
||||
var context = new TenantContext();
|
||||
context.InitializeSystem(null, "Direct DbContext construction for model tooling");
|
||||
return context;
|
||||
}
|
||||
public DbSet<Tenant> Tenants => Set<Tenant>();
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<UserIdentity> UserIdentities => Set<UserIdentity>();
|
||||
@@ -23,6 +43,7 @@ public sealed class TikuDbContext(DbContextOptions<TikuDbContext> options) : DbC
|
||||
public DbSet<TenantDomain> TenantDomains => Set<TenantDomain>();
|
||||
public DbSet<TenantBranding> TenantBrandings => Set<TenantBranding>();
|
||||
public DbSet<TenantSettings> TenantSettings => Set<TenantSettings>();
|
||||
public DbSet<TenantFrontendConfig> TenantFrontendConfigs => Set<TenantFrontendConfig>();
|
||||
public DbSet<TenantAuthProvider> TenantAuthProviders => Set<TenantAuthProvider>();
|
||||
public DbSet<TenantSecret> TenantSecrets => Set<TenantSecret>();
|
||||
public DbSet<SmsVerificationCode> SmsVerificationCodes => Set<SmsVerificationCode>();
|
||||
@@ -42,6 +63,8 @@ public sealed class TikuDbContext(DbContextOptions<TikuDbContext> options) : DbC
|
||||
public DbSet<Major> Majors => Set<Major>();
|
||||
public DbSet<Subject> Subjects => Set<Subject>();
|
||||
public DbSet<Category> Categories => Set<Category>();
|
||||
public DbSet<TaxonomyNode> TaxonomyNodes => Set<TaxonomyNode>();
|
||||
public DbSet<QuestionTaxonomyAssignment> QuestionTaxonomyAssignments => Set<QuestionTaxonomyAssignment>();
|
||||
public DbSet<ScorelineField> ScorelineFields => Set<ScorelineField>();
|
||||
public DbSet<ScorelineRecord> ScorelineRecords => Set<ScorelineRecord>();
|
||||
public DbSet<QuestionBank> QuestionBanks => Set<QuestionBank>();
|
||||
@@ -71,10 +94,11 @@ public sealed class TikuDbContext(DbContextOptions<TikuDbContext> options) : DbC
|
||||
public DbSet<AppAsset> AppAssets => Set<AppAsset>();
|
||||
public DbSet<VideoExplanation> VideoExplanations => Set<VideoExplanation>();
|
||||
public DbSet<QuestionVideo> QuestionVideos => Set<QuestionVideo>();
|
||||
public DbSet<QuestionBankGrant> QuestionBankGrants => Set<QuestionBankGrant>();
|
||||
public DbSet<TenantQuestionBankAdoption> TenantQuestionBankAdoptions => Set<TenantQuestionBankAdoption>();
|
||||
public DbSet<TenantQuestionBankPreference> TenantQuestionBankPreferences => Set<TenantQuestionBankPreference>();
|
||||
public DbSet<TenantQuestionReference> TenantQuestionReferences => Set<TenantQuestionReference>();
|
||||
public DbSet<AiRecommendationReport> AiRecommendationReports => Set<AiRecommendationReport>();
|
||||
public DbSet<PracticeSession> PracticeSessions => Set<PracticeSession>();
|
||||
public DbSet<PracticeSessionQuestion> PracticeSessionQuestions => Set<PracticeSessionQuestion>();
|
||||
public DbSet<AnswerRecord> AnswerRecords => Set<AnswerRecord>();
|
||||
public DbSet<FavoriteQuestion> FavoriteQuestions => Set<FavoriteQuestion>();
|
||||
public DbSet<WrongQuestion> WrongQuestions => Set<WrongQuestion>();
|
||||
@@ -155,9 +179,97 @@ public sealed class TikuDbContext(DbContextOptions<TikuDbContext> options) : DbC
|
||||
modelBuilder.HasPostgresExtension("citext");
|
||||
modelBuilder.HasPostgresExtension("ltree");
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(TikuDbContext).Assembly);
|
||||
ApplyTenantQueryFilters(modelBuilder);
|
||||
ValidateTenantModel(modelBuilder);
|
||||
modelBuilder.UseSnakeCaseIdentifiers();
|
||||
}
|
||||
|
||||
private void ApplyTenantQueryFilters(ModelBuilder modelBuilder)
|
||||
{
|
||||
var applyMethod = typeof(TikuDbContext)
|
||||
.GetMethod(nameof(ApplyTenantQueryFilter), BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
var tenantProperty = entityType.FindProperty("TenantId");
|
||||
if (tenantProperty?.ClrType != typeof(Guid) || entityType.BaseType is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
applyMethod.MakeGenericMethod(entityType.ClrType).Invoke(this, [modelBuilder]);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyTenantQueryFilter<TEntity>(ModelBuilder modelBuilder)
|
||||
where TEntity : class
|
||||
{
|
||||
modelBuilder.Entity<TEntity>().HasQueryFilter(entity =>
|
||||
IsSystemScope ||
|
||||
(IsTenantResolved &&
|
||||
EF.Property<Guid>(entity, "TenantId") == CurrentTenantIdOrEmpty));
|
||||
}
|
||||
|
||||
private static void ValidateTenantModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
var invalidUniqueIndexes = new List<string>();
|
||||
var invalidTenantForeignKeys = new List<string>();
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
var tenantProperty = entityType.FindProperty("TenantId");
|
||||
if (tenantProperty?.ClrType != typeof(Guid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!typeof(ITenantOwned).IsAssignableFrom(entityType.ClrType))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Tenant entity '{entityType.ClrType.Name}' must implement {nameof(ITenantOwned)}.");
|
||||
}
|
||||
|
||||
if (!entityType.GetDeclaredQueryFilters().Any())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Tenant entity '{entityType.ClrType.Name}' does not have a tenant query filter.");
|
||||
}
|
||||
|
||||
foreach (var index in entityType.GetDeclaredIndexes().Where(index => index.IsUnique))
|
||||
{
|
||||
if (index.Properties.All(property => property.Name != "TenantId") &&
|
||||
index.FindAnnotation("Tiku:GlobalUnique")?.Value is not true)
|
||||
{
|
||||
invalidUniqueIndexes.Add(
|
||||
$"{entityType.ClrType.Name}({string.Join(",", index.Properties.Select(property => property.Name))})");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
foreach (var foreignKey in entityType.GetDeclaredForeignKeys().Where(foreignKey =>
|
||||
typeof(ITenantOwned).IsAssignableFrom(foreignKey.PrincipalEntityType.ClrType)))
|
||||
{
|
||||
if (foreignKey.PrincipalKey.Properties.All(property => property.Name != "TenantId"))
|
||||
{
|
||||
invalidTenantForeignKeys.Add(
|
||||
$"{entityType.ClrType.Name}->{foreignKey.PrincipalEntityType.ClrType.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidUniqueIndexes.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Unique indexes on tenant entities must include TenantId: {string.Join("; ", invalidUniqueIndexes)}");
|
||||
}
|
||||
|
||||
|
||||
if (invalidTenantForeignKeys.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Foreign keys between tenant entities must use a tenant-qualified principal key: {string.Join("; ", invalidTenantForeignKeys)}");
|
||||
}
|
||||
}
|
||||
|
||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||
{
|
||||
UpdateTimestamps();
|
||||
|
||||
Reference in New Issue
Block a user