Files
social-app/backend/alembic/versions/20260226_0001_initial_schema.py
T
qzl e4e995854d feat: 实现密码重置功能与用户搜索API,优化注册登录流程
- 新增忘记密码页面与重置密码确认流程(前端+后端)
- 修复注册验证码页登录跳转路由
- 新增用户搜索API(按邮箱查询)
- 简化infra脚本,统一为app.sh
- 补充密码重置与用户API测试覆盖
- 更新runtime文档与AGENTS配置
2026-02-27 15:22:42 +08:00

284 lines
9.3 KiB
Python

"""initial schema part 1: foundation tables
Revision ID: 202602260001
Revises:
Create Date: 2026-02-26 20:10:00
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = "202602260001"
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"llm_factory",
sa.Column("id", sa.UUID(), nullable=False),
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("deleted_at", sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("name"),
)
_enable_rls("llm_factory")
op.create_table(
"llms",
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("deleted_at", sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("model_code"),
)
op.create_index("ix_llms_factory_id", "llms", ["factory_id"], unique=False)
op.create_foreign_key(
"fk_llms_factory_id",
"llms",
"llm_factory",
["factory_id"],
["id"],
ondelete="RESTRICT",
)
_enable_rls("llms")
op.create_table(
"profiles",
sa.Column("id", sa.UUID(), nullable=False),
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="{}",
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"),
)
op.create_index("ix_profiles_username", "profiles", ["username"], unique=False)
op.create_index(
"ix_profiles_settings_gin",
"profiles",
["settings"],
unique=False,
postgresql_using="gin",
)
op.create_foreign_key(
"fk_profiles_id",
"profiles",
"users",
["id"],
["id"],
referent_schema="auth",
ondelete="CASCADE",
)
_enable_rls("profiles")
op.execute(
"""
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
INSERT INTO public.profiles (id, username, avatar_url, bio, settings, created_at, updated_at)
VALUES (
NEW.id,
COALESCE(
NEW.raw_user_meta_data ->> 'username',
split_part(NEW.email, '@', 1),
'user_' || substring(NEW.id::text, 1, 8)
),
NULL,
NULL,
'{}'::jsonb,
now(),
now()
)
ON CONFLICT (id) DO NOTHING;
RETURN NEW;
END;
$$;
"""
)
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.create_profile_for_new_user();
"""
)
op.create_table(
"automation_jobs",
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("owner_id", sa.UUID(), nullable=False),
sa.Column("title", sa.String(length=255), nullable=False),
sa.Column("prompt", sa.Text(), nullable=False),
sa.Column("schedule_type", sa.String(length=20), nullable=False),
sa.Column("run_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("next_run_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("timezone", sa.String(length=50), nullable=False),
sa.Column("last_run_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("status", sa.String(length=20), nullable=False),
sa.Column("created_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("deleted_at", sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id", "owner_id", name="uq_automation_jobs_id_owner"),
)
op.create_index(
"ix_automation_jobs_owner_status",
"automation_jobs",
["owner_id", "status"],
unique=False,
)
op.create_index(
"ix_automation_jobs_status_next_run",
"automation_jobs",
["status", "next_run_at"],
unique=False,
)
op.execute(
"ALTER TABLE automation_jobs ADD CONSTRAINT chk_automation_job_schedule_type CHECK (schedule_type IN ('daily', 'weekly'))"
)
op.execute(
"ALTER TABLE automation_jobs ADD CONSTRAINT chk_automation_job_status CHECK (status IN ('active', 'disabled'))"
)
op.create_foreign_key(
"fk_automation_jobs_owner_id",
"automation_jobs",
"users",
["owner_id"],
["id"],
referent_schema="auth",
ondelete="CASCADE",
)
op.create_foreign_key(
"fk_automation_jobs_created_by",
"automation_jobs",
"users",
["created_by"],
["id"],
referent_schema="auth",
ondelete="SET NULL",
)
_enable_rls("automation_jobs")
op.execute("REVOKE ALL ON TABLE public.alembic_version FROM anon")
op.execute("REVOKE ALL ON TABLE public.alembic_version FROM authenticated")
def downgrade() -> None:
_drop_rls("automation_jobs")
op.drop_constraint(
"fk_automation_jobs_created_by", "automation_jobs", type_="foreignkey"
)
op.drop_constraint(
"fk_automation_jobs_owner_id", "automation_jobs", type_="foreignkey"
)
op.drop_table("automation_jobs")
_drop_rls("profiles")
op.execute("DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users")
op.execute("DROP FUNCTION IF EXISTS public.create_profile_for_new_user()")
op.drop_constraint("fk_profiles_id", "profiles", type_="foreignkey")
op.drop_table("profiles")
_drop_rls("llms")
op.drop_constraint("fk_llms_factory_id", "llms", type_="foreignkey")
op.drop_table("llms")
_drop_rls("llm_factory")
op.drop_table("llm_factory")
def _enable_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_rls(table_name: str) -> None:
for role in ["anon", "authenticated"]:
op.execute(f"DROP POLICY IF EXISTS {role}_delete_{table_name} ON {table_name}")
op.execute(f"DROP POLICY IF EXISTS {role}_update_{table_name} ON {table_name}")
op.execute(f"DROP POLICY IF EXISTS {role}_insert_{table_name} ON {table_name}")
op.execute(f"DROP POLICY IF EXISTS {role}_select_{table_name} ON {table_name}")
op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")