feat: add invite rewards and redeem codes
This commit is contained in:
@@ -27,8 +27,18 @@ def upgrade() -> None:
|
||||
sa.Column("name", sa.String(length=50), nullable=False),
|
||||
sa.Column("request_url", sa.String(length=255), nullable=False),
|
||||
sa.Column("avatar", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("name"),
|
||||
@@ -40,10 +50,25 @@ def upgrade() -> None:
|
||||
sa.Column("id", sa.UUID(), nullable=False),
|
||||
sa.Column("factory_id", sa.UUID(), nullable=False),
|
||||
sa.Column("model_code", sa.String(length=50), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(["factory_id"], ["llm_factory.id"], name="fk_llms_factory_id", ondelete="RESTRICT"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["factory_id"],
|
||||
["llm_factory.id"],
|
||||
name="fk_llms_factory_id",
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("model_code"),
|
||||
)
|
||||
@@ -55,10 +80,27 @@ def upgrade() -> None:
|
||||
sa.Column("agent_type", sa.String(length=20), nullable=False),
|
||||
sa.Column("llm_id", sa.UUID(), nullable=False),
|
||||
sa.Column("status", sa.String(length=20), nullable=False),
|
||||
sa.Column("config", postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.ForeignKeyConstraint(["llm_id"], ["llms.id"], name="fk_system_agents_llm_id", ondelete="RESTRICT"),
|
||||
sa.Column(
|
||||
"config",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default=sa.text("'{}'::jsonb"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["llm_id"], ["llms.id"], name="fk_system_agents_llm_id", ondelete="RESTRICT"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("agent_type"),
|
||||
)
|
||||
_enable_service_only_rls("system_agents")
|
||||
@@ -82,17 +124,29 @@ def downgrade() -> None:
|
||||
def _enable_service_only_rls(table_name: str) -> None:
|
||||
for role in ["anon", "authenticated"]:
|
||||
for action in ["select", "insert", "update", "delete"]:
|
||||
op.execute(f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}")
|
||||
op.execute(
|
||||
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||
)
|
||||
op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY")
|
||||
for role in ["anon", "authenticated"]:
|
||||
op.execute(f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)")
|
||||
op.execute(f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)")
|
||||
op.execute(f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)")
|
||||
op.execute(f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)")
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)"
|
||||
)
|
||||
|
||||
|
||||
def _drop_service_only_rls(table_name: str) -> None:
|
||||
for role in ["anon", "authenticated"]:
|
||||
for action in ["select", "insert", "update", "delete"]:
|
||||
op.execute(f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}")
|
||||
op.execute(
|
||||
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||
)
|
||||
op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")
|
||||
|
||||
@@ -30,7 +30,9 @@ def upgrade() -> None:
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users")
|
||||
op.execute("DROP FUNCTION IF EXISTS public.initialize_profile_and_invite_code_on_signup()")
|
||||
op.execute(
|
||||
"DROP FUNCTION IF EXISTS public.initialize_profile_and_invite_code_on_signup()"
|
||||
)
|
||||
op.execute("DROP FUNCTION IF EXISTS public.generate_invite_code()")
|
||||
|
||||
for table_name in [
|
||||
@@ -61,18 +63,37 @@ def _create_profiles() -> None:
|
||||
sa.Column("username", sa.String(length=30), nullable=False),
|
||||
sa.Column("avatar_url", sa.Text(), nullable=True),
|
||||
sa.Column("bio", sa.String(length=200), nullable=True),
|
||||
sa.Column("settings", postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
|
||||
sa.Column(
|
||||
"settings",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default=sa.text("'{}'::jsonb"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("referred_by", sa.UUID(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.CheckConstraint("char_length(username) >= 1", name="ck_profiles_username_non_empty"),
|
||||
sa.CheckConstraint(
|
||||
"char_length(username) >= 1", name="ck_profiles_username_non_empty"
|
||||
),
|
||||
sa.ForeignKeyConstraint(["id"], ["auth.users.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["referred_by"], ["profiles.id"], ondelete="SET NULL"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_profiles_username", "profiles", ["username"])
|
||||
op.create_index("ix_profiles_settings_gin", "profiles", ["settings"], postgresql_using="gin")
|
||||
op.create_index(
|
||||
"ix_profiles_settings_gin", "profiles", ["settings"], postgresql_using="gin"
|
||||
)
|
||||
op.create_index("ix_profiles_referred_by", "profiles", ["referred_by"])
|
||||
_enable_service_only_rls("profiles")
|
||||
|
||||
@@ -86,24 +107,60 @@ def _create_chat_tables() -> None:
|
||||
sa.Column("job_id", sa.UUID(), nullable=True),
|
||||
sa.Column("title", sa.String(length=255), nullable=True),
|
||||
sa.Column("status", sa.String(length=20), nullable=False),
|
||||
sa.Column("last_activity_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("message_count", sa.Integer(), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column("total_tokens", sa.Integer(), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column("total_cost", sa.Numeric(12, 6), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column("state_snapshot", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column(
|
||||
"last_activity_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"message_count", sa.Integer(), server_default=sa.text("0"), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"total_tokens", sa.Integer(), server_default=sa.text("0"), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"total_cost", sa.Numeric(12, 6), server_default=sa.text("0"), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"state_snapshot", postgresql.JSONB(astext_type=sa.Text()), nullable=True
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.CheckConstraint("session_type in ('chat', 'automation')", name="ck_sessions_session_type"),
|
||||
sa.CheckConstraint("status in ('pending', 'running', 'completed', 'failed')", name="ck_sessions_status"),
|
||||
sa.CheckConstraint("message_count >= 0", name="ck_sessions_message_count_non_negative"),
|
||||
sa.CheckConstraint("total_tokens >= 0", name="ck_sessions_total_tokens_non_negative"),
|
||||
sa.CheckConstraint("total_cost >= 0", name="ck_sessions_total_cost_non_negative"),
|
||||
sa.CheckConstraint(
|
||||
"session_type in ('chat', 'automation')", name="ck_sessions_session_type"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"status in ('pending', 'running', 'completed', 'failed')",
|
||||
name="ck_sessions_status",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"message_count >= 0", name="ck_sessions_message_count_non_negative"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"total_tokens >= 0", name="ck_sessions_total_tokens_non_negative"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"total_cost >= 0", name="ck_sessions_total_cost_non_negative"
|
||||
),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["auth.users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_sessions_user_id", "sessions", ["user_id"])
|
||||
op.create_index("ix_sessions_user_activity", "sessions", ["user_id", "last_activity_at"])
|
||||
op.create_index(
|
||||
"ix_sessions_user_activity", "sessions", ["user_id", "last_activity_at"]
|
||||
)
|
||||
_enable_service_only_rls("sessions")
|
||||
|
||||
op.create_table(
|
||||
@@ -115,27 +172,61 @@ def _create_chat_tables() -> None:
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("model_code", sa.String(length=50), nullable=True),
|
||||
sa.Column("tool_name", sa.String(length=100), nullable=True),
|
||||
sa.Column("input_tokens", sa.Integer(), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column("output_tokens", sa.Integer(), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column("cost", sa.Numeric(12, 6), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column(
|
||||
"input_tokens", sa.Integer(), server_default=sa.text("0"), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"output_tokens", sa.Integer(), server_default=sa.text("0"), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"cost", sa.Numeric(12, 6), server_default=sa.text("0"), nullable=False
|
||||
),
|
||||
sa.Column("latency_ms", sa.Integer(), nullable=True),
|
||||
sa.Column("visibility_mask", sa.BigInteger(), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column(
|
||||
"visibility_mask",
|
||||
sa.BigInteger(),
|
||||
server_default=sa.text("0"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("metadata", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.CheckConstraint("seq > 0", name="ck_messages_seq_positive"),
|
||||
sa.CheckConstraint("role in ('user', 'assistant', 'system', 'tool')", name="ck_messages_role"),
|
||||
sa.CheckConstraint("input_tokens >= 0", name="ck_messages_input_tokens_non_negative"),
|
||||
sa.CheckConstraint("output_tokens >= 0", name="ck_messages_output_tokens_non_negative"),
|
||||
sa.CheckConstraint(
|
||||
"role in ('user', 'assistant', 'system', 'tool')", name="ck_messages_role"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"input_tokens >= 0", name="ck_messages_input_tokens_non_negative"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"output_tokens >= 0", name="ck_messages_output_tokens_non_negative"
|
||||
),
|
||||
sa.CheckConstraint("cost >= 0", name="ck_messages_cost_non_negative"),
|
||||
sa.CheckConstraint("latency_ms is null or latency_ms >= 0", name="ck_messages_latency_non_negative"),
|
||||
sa.CheckConstraint(
|
||||
"latency_ms is null or latency_ms >= 0",
|
||||
name="ck_messages_latency_non_negative",
|
||||
),
|
||||
sa.ForeignKeyConstraint(["session_id"], ["sessions.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("session_id", "seq", name="uq_messages_session_seq"),
|
||||
)
|
||||
op.create_index("ix_messages_session_id", "messages", ["session_id"])
|
||||
op.create_index("ix_messages_session_seq_visibility", "messages", ["session_id", "seq", "visibility_mask"])
|
||||
op.create_index(
|
||||
"ix_messages_session_seq_visibility",
|
||||
"messages",
|
||||
["session_id", "seq", "visibility_mask"],
|
||||
)
|
||||
_enable_service_only_rls("messages")
|
||||
|
||||
|
||||
@@ -143,18 +234,53 @@ def _create_points_tables() -> None:
|
||||
op.create_table(
|
||||
"user_points",
|
||||
sa.Column("user_id", sa.UUID(), nullable=False),
|
||||
sa.Column("balance", sa.BigInteger(), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column("frozen_balance", sa.BigInteger(), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column("lifetime_earned", sa.BigInteger(), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column("lifetime_spent", sa.BigInteger(), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column(
|
||||
"balance", sa.BigInteger(), server_default=sa.text("0"), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"frozen_balance",
|
||||
sa.BigInteger(),
|
||||
server_default=sa.text("0"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"lifetime_earned",
|
||||
sa.BigInteger(),
|
||||
server_default=sa.text("0"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"lifetime_spent",
|
||||
sa.BigInteger(),
|
||||
server_default=sa.text("0"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("version", sa.Integer(), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.CheckConstraint("balance >= 0", name="ck_user_points_balance_non_negative"),
|
||||
sa.CheckConstraint("frozen_balance >= 0", name="ck_user_points_frozen_balance_non_negative"),
|
||||
sa.CheckConstraint("lifetime_earned >= 0", name="ck_user_points_lifetime_earned_non_negative"),
|
||||
sa.CheckConstraint("lifetime_spent >= 0", name="ck_user_points_lifetime_spent_non_negative"),
|
||||
sa.CheckConstraint("frozen_balance <= balance", name="ck_user_points_frozen_le_balance"),
|
||||
sa.CheckConstraint(
|
||||
"frozen_balance >= 0", name="ck_user_points_frozen_balance_non_negative"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"lifetime_earned >= 0", name="ck_user_points_lifetime_earned_non_negative"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"lifetime_spent >= 0", name="ck_user_points_lifetime_spent_non_negative"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"frozen_balance <= balance", name="ck_user_points_frozen_le_balance"
|
||||
),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["auth.users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("user_id"),
|
||||
)
|
||||
@@ -172,30 +298,89 @@ def _create_points_tables() -> None:
|
||||
sa.Column("biz_id", sa.UUID(), nullable=True),
|
||||
sa.Column("event_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("operator_id", sa.UUID(), nullable=True),
|
||||
sa.Column("metadata", postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column(
|
||||
"metadata",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default=sa.text("'{}'::jsonb"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.CheckConstraint("amount > 0", name="ck_points_ledger_amount_positive"),
|
||||
sa.CheckConstraint("direction in (1, -1)", name="ck_points_ledger_direction_valid"),
|
||||
sa.CheckConstraint("balance_after >= 0", name="ck_points_ledger_balance_after_non_negative"),
|
||||
sa.CheckConstraint("change_type in ('register', 'consume', 'adjust', 'purchase', 'refund')", name="ck_points_ledger_change_type"),
|
||||
sa.CheckConstraint("biz_type is null or biz_type in ('chat', 'payment')", name="ck_points_ledger_biz_type"),
|
||||
sa.CheckConstraint("((change_type in ('register', 'adjust') and biz_type is null and biz_id is null) or (change_type = 'consume' and biz_type = 'chat' and biz_id is not null) or (change_type in ('purchase', 'refund') and biz_type = 'payment' and biz_id is not null))", name="ck_points_ledger_biz_binding"),
|
||||
sa.CheckConstraint("((change_type in ('register', 'purchase') and direction = 1) or (change_type in ('consume', 'refund') and direction = -1) or (change_type = 'adjust' and direction in (1, -1)))", name="ck_points_ledger_direction_by_change_type"),
|
||||
sa.CheckConstraint("jsonb_typeof(metadata) = 'object'", name="ck_points_ledger_metadata_object"),
|
||||
sa.CheckConstraint("metadata->>'schema_version' = '1' and metadata->>'operator_type' in ('user', 'system', 'admin') and coalesce(metadata->>'run_id', '') <> '' and (not (metadata ? 'ext') or jsonb_typeof(metadata->'ext') = 'object')", name="ck_points_ledger_metadata_common"),
|
||||
sa.CheckConstraint("(change_type <> 'register' or not (metadata ? 'charge'))", name="ck_points_ledger_metadata_register_shape"),
|
||||
sa.CheckConstraint("(change_type <> 'consume' or ((metadata ? 'charge') and jsonb_typeof(metadata->'charge') = 'object' and (metadata->'charge' ? 'message_id') and (metadata->'charge' ? 'message_seq') and (metadata->'charge' ? 'model_code') and (metadata->'charge' ? 'input_tokens') and (metadata->'charge' ? 'output_tokens') and (metadata->'charge' ? 'cost')))", name="ck_points_ledger_metadata_consume_shape"),
|
||||
sa.CheckConstraint("(change_type <> 'adjust' or ((metadata ? 'ext') and (metadata->'ext' ? 'reason') and coalesce(metadata #>> '{ext,reason}', '') <> ''))", name="ck_points_ledger_metadata_adjust_shape"),
|
||||
sa.CheckConstraint("(change_type not in ('purchase', 'refund') or ((metadata ? 'ext') and (metadata->'ext' ? 'source') and (metadata->'ext' ? 'platform') and (metadata->'ext' ? 'product_code') and (metadata->'ext' ? 'transaction_id') and coalesce(metadata #>> '{ext,source}', '') <> '' and coalesce(metadata #>> '{ext,platform}', '') <> '' and coalesce(metadata #>> '{ext,product_code}', '') <> '' and coalesce(metadata #>> '{ext,transaction_id}', '') <> ''))", name="ck_points_ledger_metadata_payment_shape"),
|
||||
sa.CheckConstraint("(change_type <> 'refund' or ((metadata ? 'ext') and (metadata->'ext' ? 'original_event_id') and coalesce(metadata #>> '{ext,original_event_id}', '') <> ''))", name="ck_points_ledger_metadata_refund_shape"),
|
||||
sa.ForeignKeyConstraint(["operator_id"], ["auth.users.id"], ondelete="SET NULL"),
|
||||
sa.CheckConstraint(
|
||||
"direction in (1, -1)", name="ck_points_ledger_direction_valid"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"balance_after >= 0", name="ck_points_ledger_balance_after_non_negative"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"change_type in ('register', 'consume', 'adjust', 'purchase', 'refund')",
|
||||
name="ck_points_ledger_change_type",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"biz_type is null or biz_type in ('chat', 'payment')",
|
||||
name="ck_points_ledger_biz_type",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"((change_type in ('register', 'adjust') and biz_type is null and biz_id is null) or (change_type = 'consume' and biz_type = 'chat' and biz_id is not null) or (change_type in ('purchase', 'refund') and biz_type = 'payment' and biz_id is not null))",
|
||||
name="ck_points_ledger_biz_binding",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"((change_type in ('register', 'purchase') and direction = 1) or (change_type in ('consume', 'refund') and direction = -1) or (change_type = 'adjust' and direction in (1, -1)))",
|
||||
name="ck_points_ledger_direction_by_change_type",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"jsonb_typeof(metadata) = 'object'", name="ck_points_ledger_metadata_object"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"metadata->>'schema_version' = '1' and metadata->>'operator_type' in ('user', 'system', 'admin') and coalesce(metadata->>'run_id', '') <> '' and (not (metadata ? 'ext') or jsonb_typeof(metadata->'ext') = 'object')",
|
||||
name="ck_points_ledger_metadata_common",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"(change_type <> 'register' or not (metadata ? 'charge'))",
|
||||
name="ck_points_ledger_metadata_register_shape",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"(change_type <> 'consume' or ((metadata ? 'charge') and jsonb_typeof(metadata->'charge') = 'object' and (metadata->'charge' ? 'message_id') and (metadata->'charge' ? 'message_seq') and (metadata->'charge' ? 'model_code') and (metadata->'charge' ? 'input_tokens') and (metadata->'charge' ? 'output_tokens') and (metadata->'charge' ? 'cost')))",
|
||||
name="ck_points_ledger_metadata_consume_shape",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"(change_type <> 'adjust' or ((metadata ? 'ext') and (metadata->'ext' ? 'reason') and coalesce(metadata #>> '{ext,reason}', '') <> ''))",
|
||||
name="ck_points_ledger_metadata_adjust_shape",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"(change_type not in ('purchase', 'refund') or ((metadata ? 'ext') and (metadata->'ext' ? 'source') and (metadata->'ext' ? 'platform') and (metadata->'ext' ? 'product_code') and (metadata->'ext' ? 'transaction_id') and coalesce(metadata #>> '{ext,source}', '') <> '' and coalesce(metadata #>> '{ext,platform}', '') <> '' and coalesce(metadata #>> '{ext,product_code}', '') <> '' and coalesce(metadata #>> '{ext,transaction_id}', '') <> ''))",
|
||||
name="ck_points_ledger_metadata_payment_shape",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"(change_type <> 'refund' or ((metadata ? 'ext') and (metadata->'ext' ? 'original_event_id') and coalesce(metadata #>> '{ext,original_event_id}', '') <> ''))",
|
||||
name="ck_points_ledger_metadata_refund_shape",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["operator_id"], ["auth.users.id"], ondelete="SET NULL"
|
||||
),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["auth.users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("user_id", "event_id", name="uq_points_ledger_user_event"),
|
||||
)
|
||||
op.create_index("ix_points_ledger_user_created_at", "points_ledger", ["user_id", sa.text("created_at DESC")])
|
||||
op.create_index("ix_points_ledger_biz_type_biz_id", "points_ledger", ["biz_type", "biz_id"])
|
||||
op.create_index(
|
||||
"ix_points_ledger_user_created_at",
|
||||
"points_ledger",
|
||||
["user_id", sa.text("created_at DESC")],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_points_ledger_biz_type_biz_id", "points_ledger", ["biz_type", "biz_id"]
|
||||
)
|
||||
_enable_service_only_rls("points_ledger")
|
||||
|
||||
op.create_table(
|
||||
@@ -213,24 +398,71 @@ def _create_points_tables() -> None:
|
||||
sa.Column("billed_to", sa.String(length=16), nullable=False),
|
||||
sa.Column("run_id", sa.String(length=128), nullable=True),
|
||||
sa.Column("request_id", sa.String(length=128), nullable=True),
|
||||
sa.Column("input_tokens", sa.Integer(), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column("output_tokens", sa.Integer(), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column("cost", sa.Numeric(12, 6), server_default=sa.text("0"), nullable=False),
|
||||
sa.Column("metadata", postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.CheckConstraint("amount >= 0", name="ck_points_audit_ledger_amount_non_negative"),
|
||||
sa.CheckConstraint("direction in (1, 0, -1)", name="ck_points_audit_ledger_direction_valid"),
|
||||
sa.CheckConstraint("balance_after >= 0", name="ck_points_audit_ledger_balance_after_non_negative"),
|
||||
sa.CheckConstraint("change_type in ('register', 'consume', 'adjust', 'purchase', 'refund')", name="ck_points_audit_ledger_change_type"),
|
||||
sa.CheckConstraint("biz_type is null or biz_type in ('chat', 'payment')", name="ck_points_audit_ledger_biz_type"),
|
||||
sa.CheckConstraint("billed_to in ('user', 'platform')", name="ck_points_audit_ledger_billed_to"),
|
||||
sa.CheckConstraint("jsonb_typeof(metadata) = 'object'", name="ck_points_audit_ledger_metadata_object"),
|
||||
sa.Column(
|
||||
"input_tokens", sa.Integer(), server_default=sa.text("0"), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"output_tokens", sa.Integer(), server_default=sa.text("0"), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"cost", sa.Numeric(12, 6), server_default=sa.text("0"), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"metadata",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default=sa.text("'{}'::jsonb"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"amount >= 0", name="ck_points_audit_ledger_amount_non_negative"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"direction in (1, 0, -1)", name="ck_points_audit_ledger_direction_valid"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"balance_after >= 0",
|
||||
name="ck_points_audit_ledger_balance_after_non_negative",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"change_type in ('register', 'consume', 'adjust', 'purchase', 'refund')",
|
||||
name="ck_points_audit_ledger_change_type",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"biz_type is null or biz_type in ('chat', 'payment')",
|
||||
name="ck_points_audit_ledger_biz_type",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"billed_to in ('user', 'platform')", name="ck_points_audit_ledger_billed_to"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"jsonb_typeof(metadata) = 'object'",
|
||||
name="ck_points_audit_ledger_metadata_object",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("event_id", name="uq_points_audit_ledger_event_id"),
|
||||
)
|
||||
op.create_index("ix_points_audit_ledger_user_id_created_at", "points_audit_ledger", ["user_id_snapshot", sa.text("created_at DESC")])
|
||||
op.create_index("ix_points_audit_ledger_change_type_created_at", "points_audit_ledger", ["change_type", sa.text("created_at DESC")])
|
||||
op.create_index(
|
||||
"ix_points_audit_ledger_user_id_created_at",
|
||||
"points_audit_ledger",
|
||||
["user_id_snapshot", sa.text("created_at DESC")],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_points_audit_ledger_change_type_created_at",
|
||||
"points_audit_ledger",
|
||||
["change_type", sa.text("created_at DESC")],
|
||||
)
|
||||
_enable_service_only_rls("points_audit_ledger")
|
||||
|
||||
op.create_table(
|
||||
@@ -241,12 +473,29 @@ def _create_points_tables() -> None:
|
||||
sa.Column("first_user_id_snapshot", sa.UUID(), nullable=True),
|
||||
sa.Column("balance_snapshot", sa.BigInteger(), nullable=True),
|
||||
sa.Column("grant_event_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("has_purchased_starter_pack", sa.Boolean(), server_default=sa.text("false"), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column(
|
||||
"has_purchased_starter_pack",
|
||||
sa.Boolean(),
|
||||
server_default=sa.text("false"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("email_hash", name="uq_register_bonus_claims_email_hash"),
|
||||
sa.UniqueConstraint("grant_event_id", name="uq_register_bonus_claims_grant_event_id"),
|
||||
sa.UniqueConstraint(
|
||||
"grant_event_id", name="uq_register_bonus_claims_grant_event_id"
|
||||
),
|
||||
)
|
||||
_enable_service_only_rls("register_bonus_claims")
|
||||
|
||||
@@ -269,7 +518,9 @@ def _create_invite_codes() -> None:
|
||||
"""
|
||||
)
|
||||
op.execute("CREATE INDEX ix_invite_codes_owner_id ON invite_codes(owner_id)")
|
||||
op.execute("CREATE INDEX ix_invite_codes_code ON invite_codes(code) WHERE status = 'active'")
|
||||
op.execute(
|
||||
"CREATE INDEX ix_invite_codes_code ON invite_codes(code) WHERE status = 'active'"
|
||||
)
|
||||
_enable_service_only_rls("invite_codes")
|
||||
|
||||
|
||||
@@ -370,23 +621,37 @@ def _create_signup_helpers() -> None:
|
||||
"""
|
||||
)
|
||||
op.execute("DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users")
|
||||
op.execute("CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.initialize_profile_and_invite_code_on_signup()")
|
||||
op.execute(
|
||||
"CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.initialize_profile_and_invite_code_on_signup()"
|
||||
)
|
||||
|
||||
|
||||
def _enable_service_only_rls(table_name: str) -> None:
|
||||
for role in ["anon", "authenticated"]:
|
||||
for action in ["select", "insert", "update", "delete"]:
|
||||
op.execute(f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}")
|
||||
op.execute(
|
||||
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||
)
|
||||
op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY")
|
||||
for role in ["anon", "authenticated"]:
|
||||
op.execute(f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)")
|
||||
op.execute(f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)")
|
||||
op.execute(f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)")
|
||||
op.execute(f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)")
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)"
|
||||
)
|
||||
|
||||
|
||||
def _drop_service_only_rls(table_name: str) -> None:
|
||||
for role in ["anon", "authenticated"]:
|
||||
for action in ["select", "insert", "update", "delete"]:
|
||||
op.execute(f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}")
|
||||
op.execute(
|
||||
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||
)
|
||||
op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")
|
||||
|
||||
@@ -23,29 +23,90 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"notifications",
|
||||
sa.Column("id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False),
|
||||
sa.Column("type", sa.String(length=32), server_default=sa.text("'system'"), nullable=False),
|
||||
sa.Column("source", sa.String(length=32), server_default=sa.text("'manual'"), nullable=False),
|
||||
sa.Column(
|
||||
"id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"type",
|
||||
sa.String(length=32),
|
||||
server_default=sa.text("'system'"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"source",
|
||||
sa.String(length=32),
|
||||
server_default=sa.text("'manual'"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("source_key", sa.String(length=128), nullable=True),
|
||||
sa.Column("source_version", sa.Integer(), nullable=True),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=True),
|
||||
sa.Column("title", postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
|
||||
sa.Column("body", postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
|
||||
sa.Column("payload", postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
|
||||
sa.Column("status", sa.String(length=16), server_default=sa.text("'published'"), nullable=False),
|
||||
sa.Column("target_mode", sa.String(length=32), server_default=sa.text("'all_users'"), nullable=False),
|
||||
sa.Column(
|
||||
"title",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default=sa.text("'{}'::jsonb"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"body",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default=sa.text("'{}'::jsonb"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"payload",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default=sa.text("'{}'::jsonb"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.String(length=16),
|
||||
server_default=sa.text("'published'"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"target_mode",
|
||||
sa.String(length=32),
|
||||
server_default=sa.text("'all_users'"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("published_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.CheckConstraint("status IN ('draft', 'published', 'revoked')", name="ck_notifications_status"),
|
||||
sa.CheckConstraint("target_mode IN ('new_users', 'exist_users', 'all_users', 'user_ids')", name="ck_notifications_target_mode"),
|
||||
sa.CheckConstraint("jsonb_typeof(payload) = 'object'", name="ck_notifications_payload_object"),
|
||||
sa.CheckConstraint(
|
||||
"status IN ('draft', 'published', 'revoked')",
|
||||
name="ck_notifications_status",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"target_mode IN ('new_users', 'exist_users', 'all_users', 'user_ids')",
|
||||
name="ck_notifications_target_mode",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"jsonb_typeof(payload) = 'object'", name="ck_notifications_payload_object"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_notifications_status_created_at", "notifications", ["status", sa.text("created_at DESC")])
|
||||
op.create_index("ix_notifications_published_at", "notifications", [sa.text("published_at DESC")])
|
||||
op.create_index(
|
||||
"ix_notifications_status_created_at",
|
||||
"notifications",
|
||||
["status", sa.text("created_at DESC")],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_notifications_published_at", "notifications", [sa.text("published_at DESC")]
|
||||
)
|
||||
op.create_index(
|
||||
"uq_notifications_source_source_key",
|
||||
"notifications",
|
||||
@@ -57,20 +118,46 @@ def upgrade() -> None:
|
||||
|
||||
op.create_table(
|
||||
"user_notifications",
|
||||
sa.Column("id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False),
|
||||
sa.Column(
|
||||
"id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False
|
||||
),
|
||||
sa.Column("user_id", sa.UUID(), nullable=False),
|
||||
sa.Column("notification_id", sa.UUID(), nullable=False),
|
||||
sa.Column("is_read", sa.Boolean(), server_default=sa.text("false"), nullable=False),
|
||||
sa.Column(
|
||||
"is_read", sa.Boolean(), server_default=sa.text("false"), nullable=False
|
||||
),
|
||||
sa.Column("read_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["auth.users.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["notification_id"], ["notifications.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["notification_id"], ["notifications.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("user_id", "notification_id", name="uq_user_notifications_user_notification"),
|
||||
sa.UniqueConstraint(
|
||||
"user_id", "notification_id", name="uq_user_notifications_user_notification"
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_user_notifications_user_created_at",
|
||||
"user_notifications",
|
||||
["user_id", sa.text("created_at DESC")],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_user_notifications_user_unread",
|
||||
"user_notifications",
|
||||
["user_id", "is_read"],
|
||||
)
|
||||
op.create_index("ix_user_notifications_user_created_at", "user_notifications", ["user_id", sa.text("created_at DESC")])
|
||||
op.create_index("ix_user_notifications_user_unread", "user_notifications", ["user_id", "is_read"])
|
||||
_enable_service_only_rls("user_notifications")
|
||||
|
||||
|
||||
@@ -85,17 +172,29 @@ def downgrade() -> None:
|
||||
def _enable_service_only_rls(table_name: str) -> None:
|
||||
for role in ["anon", "authenticated"]:
|
||||
for action in ["select", "insert", "update", "delete"]:
|
||||
op.execute(f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}")
|
||||
op.execute(
|
||||
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||
)
|
||||
op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY")
|
||||
for role in ["anon", "authenticated"]:
|
||||
op.execute(f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)")
|
||||
op.execute(f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)")
|
||||
op.execute(f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)")
|
||||
op.execute(f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)")
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)"
|
||||
)
|
||||
|
||||
|
||||
def _drop_service_only_rls(table_name: str) -> None:
|
||||
for role in ["anon", "authenticated"]:
|
||||
for action in ["select", "insert", "update", "delete"]:
|
||||
op.execute(f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}")
|
||||
op.execute(
|
||||
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||
)
|
||||
op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")
|
||||
|
||||
@@ -42,27 +42,79 @@ def upgrade() -> None:
|
||||
sa.Column("total_latency_ms", sa.Integer(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("last_activity_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("anonymized_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column(
|
||||
"anonymized_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_anonymous_session_snapshots_anonymous_id", "anonymous_session_snapshots", ["anonymous_id"])
|
||||
op.create_index("ix_anonymous_session_snapshots_created_at", "anonymous_session_snapshots", ["created_at"])
|
||||
op.create_index("ix_anonymous_session_snapshots_question_type", "anonymous_session_snapshots", ["question_type"])
|
||||
op.create_index(
|
||||
"ix_anonymous_session_snapshots_anonymous_id",
|
||||
"anonymous_session_snapshots",
|
||||
["anonymous_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_anonymous_session_snapshots_created_at",
|
||||
"anonymous_session_snapshots",
|
||||
["created_at"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_anonymous_session_snapshots_question_type",
|
||||
"anonymous_session_snapshots",
|
||||
["question_type"],
|
||||
)
|
||||
_enable_service_role_all_rls("anonymous_session_snapshots")
|
||||
|
||||
op.create_table(
|
||||
"user_feedback",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), nullable=False),
|
||||
sa.Column(
|
||||
"id",
|
||||
postgresql.UUID(as_uuid=True),
|
||||
server_default=sa.text("gen_random_uuid()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column("feedback_type", sa.String(length=20), server_default=sa.text("'other'"), nullable=False),
|
||||
sa.Column(
|
||||
"feedback_type",
|
||||
sa.String(length=20),
|
||||
server_default=sa.text("'other'"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("images", postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'[]'::jsonb"), nullable=False),
|
||||
sa.Column("device_info", postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
|
||||
sa.Column(
|
||||
"images",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default=sa.text("'[]'::jsonb"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"device_info",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default=sa.text("'{}'::jsonb"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("app_version", sa.String(length=20), nullable=False),
|
||||
sa.Column("os_version", sa.String(length=50), nullable=False),
|
||||
sa.Column("status", sa.String(length=20), server_default=sa.text("'pending'"), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.String(length=20),
|
||||
server_default=sa.text("'pending'"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["auth.users.id"], ondelete="SET NULL"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
@@ -70,12 +122,22 @@ def upgrade() -> None:
|
||||
op.create_index("ix_user_feedback_created_at", "user_feedback", ["created_at"])
|
||||
op.create_index("ix_user_feedback_status", "user_feedback", ["status"])
|
||||
op.execute("COMMENT ON TABLE user_feedback IS '用户反馈表'")
|
||||
op.execute("COMMENT ON COLUMN user_feedback.user_id IS '用户ID,NULL表示匿名(勾选不上传我的个人信息)'")
|
||||
op.execute("COMMENT ON COLUMN user_feedback.feedback_type IS '反馈类型: bug/suggestion/other'")
|
||||
op.execute(
|
||||
"COMMENT ON COLUMN user_feedback.user_id IS '用户ID,NULL表示匿名(勾选不上传我的个人信息)'"
|
||||
)
|
||||
op.execute(
|
||||
"COMMENT ON COLUMN user_feedback.feedback_type IS '反馈类型: bug/suggestion/other'"
|
||||
)
|
||||
op.execute("COMMENT ON COLUMN user_feedback.content IS '反馈内容,最多500字'")
|
||||
op.execute("COMMENT ON COLUMN user_feedback.images IS '图片Storage路径列表,最多3张'")
|
||||
op.execute("COMMENT ON COLUMN user_feedback.device_info IS '设备信息JSON,匿名时照样采集(不涉及隐私)'")
|
||||
op.execute("COMMENT ON COLUMN user_feedback.status IS '处理状态: pending/processed'")
|
||||
op.execute(
|
||||
"COMMENT ON COLUMN user_feedback.images IS '图片Storage路径列表,最多3张'"
|
||||
)
|
||||
op.execute(
|
||||
"COMMENT ON COLUMN user_feedback.device_info IS '设备信息JSON,匿名时照样采集(不涉及隐私)'"
|
||||
)
|
||||
op.execute(
|
||||
"COMMENT ON COLUMN user_feedback.status IS '处理状态: pending/processed'"
|
||||
)
|
||||
_enable_service_role_all_rls("user_feedback")
|
||||
|
||||
|
||||
@@ -89,7 +151,9 @@ def downgrade() -> None:
|
||||
|
||||
def _enable_service_role_all_rls(table_name: str) -> None:
|
||||
op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY")
|
||||
op.execute(f"CREATE POLICY service_role_all_{table_name} ON {table_name} FOR ALL TO service_role USING (true) WITH CHECK (true)")
|
||||
op.execute(
|
||||
f"CREATE POLICY service_role_all_{table_name} ON {table_name} FOR ALL TO service_role USING (true) WITH CHECK (true)"
|
||||
)
|
||||
|
||||
|
||||
def _drop_service_role_all_rls(table_name: str) -> None:
|
||||
|
||||
@@ -42,18 +42,51 @@ def upgrade() -> None:
|
||||
sa.Column("price_milliunits", sa.BigInteger(), nullable=True),
|
||||
sa.Column("ledger_event_id", sa.String(length=64), nullable=True),
|
||||
sa.Column("signed_transaction_info", sa.Text(), nullable=False),
|
||||
sa.Column("apple_payload", postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
|
||||
sa.Column(
|
||||
"apple_payload",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default=sa.text("'{}'::jsonb"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("failure_code", sa.String(length=64), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.CheckConstraint("environment in ('Sandbox', 'Production')", name="ck_apple_iap_transactions_environment"),
|
||||
sa.CheckConstraint("status in ('received', 'verified', 'granted', 'failed', 'refunded', 'refunded_insufficient', 'revoked')", name="ck_apple_iap_transactions_status"),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"environment in ('Sandbox', 'Production')",
|
||||
name="ck_apple_iap_transactions_environment",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"status in ('received', 'verified', 'granted', 'failed', 'refunded', 'refunded_insufficient', 'revoked')",
|
||||
name="ck_apple_iap_transactions_status",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("transaction_id", name="uq_apple_iap_transactions_transaction_id"),
|
||||
sa.UniqueConstraint("ledger_event_id", name="uq_apple_iap_transactions_ledger_event_id"),
|
||||
sa.UniqueConstraint(
|
||||
"transaction_id", name="uq_apple_iap_transactions_transaction_id"
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"ledger_event_id", name="uq_apple_iap_transactions_ledger_event_id"
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_apple_iap_transactions_user_created_at",
|
||||
"apple_iap_transactions",
|
||||
["user_id", sa.text("created_at DESC")],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_apple_iap_transactions_status_updated_at",
|
||||
"apple_iap_transactions",
|
||||
["status", sa.text("updated_at DESC")],
|
||||
)
|
||||
op.create_index("ix_apple_iap_transactions_user_created_at", "apple_iap_transactions", ["user_id", sa.text("created_at DESC")])
|
||||
op.create_index("ix_apple_iap_transactions_status_updated_at", "apple_iap_transactions", ["status", sa.text("updated_at DESC")])
|
||||
_enable_service_only_rls("apple_iap_transactions")
|
||||
|
||||
|
||||
@@ -65,17 +98,29 @@ def downgrade() -> None:
|
||||
def _enable_service_only_rls(table_name: str) -> None:
|
||||
for role in ["anon", "authenticated"]:
|
||||
for action in ["select", "insert", "update", "delete"]:
|
||||
op.execute(f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}")
|
||||
op.execute(
|
||||
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||
)
|
||||
op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY")
|
||||
for role in ["anon", "authenticated"]:
|
||||
op.execute(f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)")
|
||||
op.execute(f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)")
|
||||
op.execute(f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)")
|
||||
op.execute(f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)")
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)"
|
||||
)
|
||||
|
||||
|
||||
def _drop_service_only_rls(table_name: str) -> None:
|
||||
for role in ["anon", "authenticated"]:
|
||||
for action in ["select", "insert", "update", "delete"]:
|
||||
op.execute(f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}")
|
||||
op.execute(
|
||||
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||
)
|
||||
op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")
|
||||
|
||||
@@ -31,16 +31,42 @@ def upgrade() -> None:
|
||||
sa.Column("credits", sa.BigInteger(), nullable=False),
|
||||
sa.Column("amount_cents", sa.BigInteger(), nullable=False),
|
||||
sa.Column("currency", sa.String(length=8), nullable=False),
|
||||
sa.Column("creem_payload", postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
|
||||
sa.Column(
|
||||
"creem_payload",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default=sa.text("'{}'::jsonb"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("ledger_event_id", sa.String(length=128), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.CheckConstraint("status in ('pending', 'completed', 'failed', 'refunded')", name="ck_creem_transactions_status"),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"status in ('pending', 'completed', 'failed', 'refunded')",
|
||||
name="ck_creem_transactions_status",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("checkout_id", name="uq_creem_transactions_checkout_id"),
|
||||
)
|
||||
op.create_index("ix_creem_transactions_user_created_at", "creem_transactions", ["user_id", sa.text("created_at DESC")])
|
||||
op.create_index("ix_creem_transactions_status_updated_at", "creem_transactions", ["status", sa.text("updated_at DESC")])
|
||||
op.create_index(
|
||||
"ix_creem_transactions_user_created_at",
|
||||
"creem_transactions",
|
||||
["user_id", sa.text("created_at DESC")],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_creem_transactions_status_updated_at",
|
||||
"creem_transactions",
|
||||
["status", sa.text("updated_at DESC")],
|
||||
)
|
||||
_enable_service_only_rls("creem_transactions")
|
||||
|
||||
|
||||
@@ -52,17 +78,29 @@ def downgrade() -> None:
|
||||
def _enable_service_only_rls(table_name: str) -> None:
|
||||
for role in ["anon", "authenticated"]:
|
||||
for action in ["select", "insert", "update", "delete"]:
|
||||
op.execute(f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}")
|
||||
op.execute(
|
||||
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||
)
|
||||
op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY")
|
||||
for role in ["anon", "authenticated"]:
|
||||
op.execute(f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)")
|
||||
op.execute(f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)")
|
||||
op.execute(f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)")
|
||||
op.execute(f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)")
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)"
|
||||
)
|
||||
|
||||
|
||||
def _drop_service_only_rls(table_name: str) -> None:
|
||||
for role in ["anon", "authenticated"]:
|
||||
for action in ["select", "insert", "update", "delete"]:
|
||||
op.execute(f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}")
|
||||
op.execute(
|
||||
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||
)
|
||||
op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
"""Create invite referral, redeem code, and system audit tables.
|
||||
|
||||
Revision ID: 20260521_0002
|
||||
Revises: 20260511_0001
|
||||
Create Date: 2026-05-21 16:00:00
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision: str = "20260521_0002"
|
||||
down_revision: Union[str, Sequence[str], None] = "20260511_0001"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"invite_referrals",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("inviter_user_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("invitee_user_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("invite_code_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column("invite_code_snapshot", sa.String(length=6), nullable=False),
|
||||
sa.Column(
|
||||
"bound_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"first_creem_transaction_id", postgresql.UUID(as_uuid=True), nullable=True
|
||||
),
|
||||
sa.Column("first_creem_paid_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("inviter_reward_event_id", sa.String(length=64), nullable=True),
|
||||
sa.Column("invitee_reward_event_id", sa.String(length=64), nullable=True),
|
||||
sa.Column(
|
||||
"inviter_reward_granted_at", sa.DateTime(timezone=True), nullable=True
|
||||
),
|
||||
sa.Column(
|
||||
"invitee_reward_granted_at", sa.DateTime(timezone=True), nullable=True
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["first_creem_transaction_id"],
|
||||
["creem_transactions.id"],
|
||||
ondelete="SET NULL",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["invite_code_id"], ["invite_codes.id"], ondelete="SET NULL"
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["invitee_user_id"], ["profiles.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["inviter_user_id"], ["profiles.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.CheckConstraint(
|
||||
"inviter_user_id <> invitee_user_id", name="ck_invite_referrals_not_self"
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"invitee_user_id", name="uq_invite_referrals_invitee_user_id"
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"inviter_reward_event_id",
|
||||
name="uq_invite_referrals_inviter_reward_event_id",
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"invitee_reward_event_id",
|
||||
name="uq_invite_referrals_invitee_reward_event_id",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_invite_referrals_inviter_bound_at",
|
||||
"invite_referrals",
|
||||
["inviter_user_id", sa.text("bound_at DESC")],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_invite_referrals_invitee_bound_at",
|
||||
"invite_referrals",
|
||||
["invitee_user_id", sa.text("bound_at DESC")],
|
||||
)
|
||||
_enable_service_only_rls("invite_referrals")
|
||||
|
||||
op.create_table(
|
||||
"redeem_code_batches",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("batch_key", sa.String(length=64), nullable=False),
|
||||
sa.Column("created_by", sa.String(length=64), nullable=True),
|
||||
sa.Column("notes", sa.Text(), nullable=True),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("batch_key", name="uq_redeem_code_batches_batch_key"),
|
||||
)
|
||||
_enable_service_only_rls("redeem_code_batches")
|
||||
|
||||
op.create_table(
|
||||
"redeem_codes",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("batch_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("code", sa.String(length=32), nullable=False),
|
||||
sa.Column("package_product_code", sa.String(length=32), nullable=False),
|
||||
sa.Column("package_type", sa.String(length=16), nullable=False),
|
||||
sa.Column("package_name_snapshot", sa.String(length=64), nullable=False),
|
||||
sa.Column("credits", sa.BigInteger(), nullable=False),
|
||||
sa.Column("sort_order", sa.BigInteger(), nullable=False),
|
||||
sa.Column("status", sa.String(length=16), nullable=False),
|
||||
sa.Column("redeemed_by_user_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column("redeemed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("redeem_event_id", sa.String(length=64), nullable=True),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["batch_id"], ["redeem_code_batches.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["redeemed_by_user_id"], ["auth.users.id"], ondelete="SET NULL"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("code"),
|
||||
sa.CheckConstraint(
|
||||
"status in ('active', 'redeemed', 'disabled')",
|
||||
name="ck_redeem_codes_status",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"((status = 'redeemed' and redeemed_by_user_id is not null and redeemed_at is not null and redeem_event_id is not null) or (status <> 'redeemed'))",
|
||||
name="ck_redeem_codes_redeemed_shape",
|
||||
),
|
||||
)
|
||||
op.create_index("ix_redeem_codes_code", "redeem_codes", ["code"], unique=True)
|
||||
op.create_index(
|
||||
"ix_redeem_codes_batch_status", "redeem_codes", ["batch_id", "status"]
|
||||
)
|
||||
op.create_index(
|
||||
"ix_redeem_codes_redeemed_by",
|
||||
"redeem_codes",
|
||||
["redeemed_by_user_id", sa.text("redeemed_at DESC")],
|
||||
)
|
||||
_enable_service_only_rls("redeem_codes")
|
||||
|
||||
op.create_table(
|
||||
"system_audit_logs",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("actor_user_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column("target_user_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column("action", sa.String(length=64), nullable=False),
|
||||
sa.Column("entity_type", sa.String(length=32), nullable=False),
|
||||
sa.Column("entity_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column(
|
||||
"metadata",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default=sa.text("'{}'::jsonb"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.CheckConstraint(
|
||||
"jsonb_typeof(metadata) = 'object'",
|
||||
name="ck_system_audit_logs_metadata_object",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_system_audit_logs_action_created_at",
|
||||
"system_audit_logs",
|
||||
["action", sa.text("created_at DESC")],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_system_audit_logs_actor_created_at",
|
||||
"system_audit_logs",
|
||||
["actor_user_id", sa.text("created_at DESC")],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_system_audit_logs_target_created_at",
|
||||
"system_audit_logs",
|
||||
["target_user_id", sa.text("created_at DESC")],
|
||||
)
|
||||
_enable_service_only_rls("system_audit_logs")
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO invite_referrals (
|
||||
id,
|
||||
inviter_user_id,
|
||||
invitee_user_id,
|
||||
invite_code_id,
|
||||
invite_code_snapshot,
|
||||
bound_at,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
p.referred_by,
|
||||
p.id,
|
||||
ic.id,
|
||||
ic.code,
|
||||
p.created_at,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM profiles p
|
||||
JOIN LATERAL (
|
||||
SELECT id, code
|
||||
FROM invite_codes
|
||||
WHERE owner_id = p.referred_by
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
) ic ON TRUE
|
||||
WHERE p.referred_by IS NOT NULL
|
||||
ON CONFLICT (invitee_user_id) DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
def downgrade() -> None:
|
||||
_drop_service_only_rls("system_audit_logs")
|
||||
op.drop_table("system_audit_logs")
|
||||
|
||||
_drop_service_only_rls("redeem_codes")
|
||||
op.drop_index("ix_redeem_codes_redeemed_by", table_name="redeem_codes")
|
||||
op.drop_index("ix_redeem_codes_batch_status", table_name="redeem_codes")
|
||||
op.drop_index("ix_redeem_codes_code", table_name="redeem_codes")
|
||||
op.drop_table("redeem_codes")
|
||||
|
||||
_drop_service_only_rls("redeem_code_batches")
|
||||
op.drop_table("redeem_code_batches")
|
||||
|
||||
_drop_service_only_rls("invite_referrals")
|
||||
op.drop_index("ix_invite_referrals_invitee_bound_at", table_name="invite_referrals")
|
||||
op.drop_index("ix_invite_referrals_inviter_bound_at", table_name="invite_referrals")
|
||||
op.drop_table("invite_referrals")
|
||||
|
||||
|
||||
def _enable_service_only_rls(table_name: str) -> None:
|
||||
for role in ["anon", "authenticated"]:
|
||||
for action in ["select", "insert", "update", "delete"]:
|
||||
op.execute(
|
||||
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||
)
|
||||
op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY")
|
||||
for role in ["anon", "authenticated"]:
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)"
|
||||
)
|
||||
|
||||
|
||||
def _drop_service_only_rls(table_name: str) -> None:
|
||||
for role in ["anon", "authenticated"]:
|
||||
for action in ["select", "insert", "update", "delete"]:
|
||||
op.execute(
|
||||
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||
)
|
||||
op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")
|
||||
Reference in New Issue
Block a user