"""foundation: users, groups, group_members, shares Revision ID: 0001 Revises: Create Date: 2026-07-19 Identity and the sharing ACL. The shares table is polymorphic, so any shareable entity uses it. """ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects.postgresql import CITEXT, UUID revision = "0001" down_revision = None branch_labels = None depends_on = None def upgrade() -> None: op.execute("CREATE EXTENSION IF NOT EXISTS citext") op.create_table( "users", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("email", CITEXT(), nullable=False, unique=True), sa.Column("email_verified", sa.Boolean(), nullable=False, server_default=sa.false()), sa.Column("password_hash", sa.Text(), nullable=True), sa.Column("display_name", sa.Text(), nullable=False), sa.Column("avatar_path", sa.Text(), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) op.create_index("ix_users_email", "users", ["email"]) op.create_table( "groups", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("name", sa.Text(), nullable=False), sa.Column("owner_id", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) op.create_table( "group_members", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("group_id", UUID(as_uuid=True), sa.ForeignKey("groups.id", ondelete="CASCADE"), nullable=False), sa.Column("user_id", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False), sa.Column("role", sa.Text(), nullable=False, server_default="member"), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.UniqueConstraint("group_id", "user_id", name="uq_group_members_group_user"), ) op.create_index("ix_group_members_user", "group_members", ["user_id"]) op.create_table( "shares", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("resource_type", sa.Text(), nullable=False), sa.Column("resource_id", UUID(as_uuid=True), nullable=False), sa.Column("shared_with_user_id", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=True), sa.Column("shared_with_group_id", UUID(as_uuid=True), sa.ForeignKey("groups.id", ondelete="CASCADE"), nullable=True), sa.Column("permission", sa.Text(), nullable=False, server_default="view"), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.CheckConstraint( "(shared_with_user_id IS NOT NULL) <> (shared_with_group_id IS NOT NULL)", name="ck_shares_one_target", ), ) op.create_index("ix_shares_resource", "shares", ["resource_type", "resource_id"]) def downgrade() -> None: op.drop_index("ix_shares_resource", table_name="shares") op.drop_table("shares") op.drop_index("ix_group_members_user", table_name="group_members") op.drop_table("group_members") op.drop_table("groups") op.drop_index("ix_users_email", table_name="users") op.drop_table("users") # citext extension is left installed — it may be in use elsewhere.