489 lines
16 KiB
Python
489 lines
16 KiB
Python
"""initial schema part 4: collaboration tables
|
|
|
|
Revision ID: 202602260004
|
|
Revises: 202602260003
|
|
Create Date: 2026-02-26 20:13:00
|
|
"""
|
|
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
revision: str = "202602260004"
|
|
down_revision: Union[str, Sequence[str], None] = "202602260003"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"schedule_items",
|
|
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("description", sa.Text(), nullable=True),
|
|
sa.Column("start_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("end_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column("timezone", sa.String(length=50), nullable=False),
|
|
sa.Column(
|
|
"metadata",
|
|
postgresql.JSONB(astext_type=sa.Text()),
|
|
server_default="{}",
|
|
nullable=False,
|
|
),
|
|
sa.Column("recurrence_rule", sa.String(length=255), nullable=True),
|
|
sa.Column("source_type", sa.String(length=20), nullable=False),
|
|
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"),
|
|
)
|
|
op.create_index(
|
|
"ix_schedule_items_owner_start",
|
|
"schedule_items",
|
|
["owner_id", "start_at"],
|
|
unique=False,
|
|
)
|
|
op.create_index(
|
|
"ix_schedule_items_status_start",
|
|
"schedule_items",
|
|
["status", "start_at"],
|
|
unique=False,
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE schedule_items ADD CONSTRAINT chk_schedule_item_source_type CHECK (source_type IN ('manual', 'imported', 'agent_generated'))"
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE schedule_items ADD CONSTRAINT chk_schedule_item_status CHECK (status IN ('active', 'completed', 'canceled', 'archived'))"
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_schedule_items_owner_id",
|
|
"schedule_items",
|
|
"users",
|
|
["owner_id"],
|
|
["id"],
|
|
referent_schema="auth",
|
|
ondelete="CASCADE",
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_schedule_items_created_by",
|
|
"schedule_items",
|
|
"users",
|
|
["created_by"],
|
|
["id"],
|
|
referent_schema="auth",
|
|
ondelete="SET NULL",
|
|
)
|
|
_enable_rls("schedule_items")
|
|
|
|
op.create_table(
|
|
"schedule_subscriptions",
|
|
sa.Column("id", sa.UUID(), nullable=False),
|
|
sa.Column("item_id", sa.UUID(), nullable=False),
|
|
sa.Column("subscriber_id", sa.UUID(), nullable=False),
|
|
sa.Column(
|
|
"permission", sa.Integer(), nullable=False, server_default=sa.text("1")
|
|
),
|
|
sa.Column(
|
|
"notify_level",
|
|
sa.String(length=20),
|
|
nullable=False,
|
|
server_default=sa.text("'all'"),
|
|
),
|
|
sa.Column(
|
|
"status",
|
|
sa.String(length=20),
|
|
nullable=False,
|
|
server_default=sa.text("'active'"),
|
|
),
|
|
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.PrimaryKeyConstraint("id"),
|
|
sa.UniqueConstraint(
|
|
"item_id", "subscriber_id", name="uq_schedule_subscriptions_item_subscriber"
|
|
),
|
|
)
|
|
op.create_index(
|
|
"ix_schedule_subscribers_subscriber_status",
|
|
"schedule_subscriptions",
|
|
["subscriber_id", "status"],
|
|
unique=False,
|
|
)
|
|
op.create_index(
|
|
"ix_schedule_subscribers_item_status",
|
|
"schedule_subscriptions",
|
|
["item_id", "status"],
|
|
unique=False,
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE schedule_subscriptions ADD CONSTRAINT chk_schedule_subscription_permission CHECK (permission BETWEEN 0 AND 7)"
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE schedule_subscriptions ADD CONSTRAINT chk_schedule_subscription_notify_level CHECK (notify_level IN ('all', 'mentions', 'none'))"
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE schedule_subscriptions ADD CONSTRAINT chk_schedule_subscription_status CHECK (status IN ('active', 'paused', 'unsubscribed', 'pending'))"
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_schedule_subscriptions_item_id",
|
|
"schedule_subscriptions",
|
|
"schedule_items",
|
|
["item_id"],
|
|
["id"],
|
|
ondelete="CASCADE",
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_schedule_subscriptions_subscriber_id",
|
|
"schedule_subscriptions",
|
|
"users",
|
|
["subscriber_id"],
|
|
["id"],
|
|
referent_schema="auth",
|
|
ondelete="CASCADE",
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_schedule_subscriptions_created_by",
|
|
"schedule_subscriptions",
|
|
"users",
|
|
["created_by"],
|
|
["id"],
|
|
referent_schema="auth",
|
|
ondelete="SET NULL",
|
|
)
|
|
_enable_rls("schedule_subscriptions")
|
|
|
|
op.create_table(
|
|
"todos",
|
|
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("description", sa.String(length=1000), nullable=True),
|
|
sa.Column("due_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column("priority", sa.Integer(), nullable=False),
|
|
sa.Column("completed_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"),
|
|
)
|
|
op.create_index(
|
|
"ix_todos_owner_status_due",
|
|
"todos",
|
|
["owner_id", "status", "due_at"],
|
|
unique=False,
|
|
)
|
|
op.create_index(
|
|
"ix_todos_owner_created", "todos", ["owner_id", "created_at"], unique=False
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE todos ADD CONSTRAINT chk_todos_status CHECK (status IN ('pending', 'done', 'canceled'))"
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE todos ADD CONSTRAINT chk_todos_priority CHECK (priority BETWEEN 1 AND 4)"
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_todos_owner_id",
|
|
"todos",
|
|
"users",
|
|
["owner_id"],
|
|
["id"],
|
|
referent_schema="auth",
|
|
ondelete="CASCADE",
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_todos_created_by",
|
|
"todos",
|
|
"users",
|
|
["created_by"],
|
|
["id"],
|
|
referent_schema="auth",
|
|
ondelete="SET NULL",
|
|
)
|
|
_enable_rls("todos")
|
|
|
|
op.create_table(
|
|
"todo_sources",
|
|
sa.Column("id", sa.UUID(), nullable=False),
|
|
sa.Column("todo_id", sa.UUID(), nullable=False),
|
|
sa.Column("schedule_item_id", sa.UUID(), 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(
|
|
"todo_id", "schedule_item_id", name="uq_todo_sources_todo_schedule"
|
|
),
|
|
)
|
|
op.create_index("ix_todo_sources_todo", "todo_sources", ["todo_id"], unique=False)
|
|
op.create_index(
|
|
"ix_todo_sources_schedule_item",
|
|
"todo_sources",
|
|
["schedule_item_id"],
|
|
unique=False,
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_todo_sources_todo_id",
|
|
"todo_sources",
|
|
"todos",
|
|
["todo_id"],
|
|
["id"],
|
|
ondelete="CASCADE",
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_todo_sources_schedule_item_id",
|
|
"todo_sources",
|
|
"schedule_items",
|
|
["schedule_item_id"],
|
|
["id"],
|
|
ondelete="CASCADE",
|
|
)
|
|
_enable_rls("todo_sources")
|
|
|
|
op.create_table(
|
|
"inbox_messages",
|
|
sa.Column("id", sa.UUID(), nullable=False),
|
|
sa.Column("recipient_id", sa.UUID(), nullable=False),
|
|
sa.Column("sender_id", sa.UUID(), nullable=True),
|
|
sa.Column("message_type", sa.String(length=20), nullable=False),
|
|
sa.Column("friendship_id", sa.UUID(), nullable=True),
|
|
sa.Column("schedule_item_id", sa.UUID(), nullable=True),
|
|
sa.Column("group_id", sa.UUID(), nullable=True),
|
|
sa.Column("content", sa.Text(), nullable=True),
|
|
sa.Column(
|
|
"is_read", sa.Boolean(), nullable=False, server_default=sa.text("false")
|
|
),
|
|
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.PrimaryKeyConstraint("id"),
|
|
)
|
|
op.create_index(
|
|
"ix_inbox_messages_recipient_status_created",
|
|
"inbox_messages",
|
|
["recipient_id", "status", "created_at"],
|
|
unique=False,
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_type CHECK (message_type IN ('friend_request', 'calendar', 'system', 'group'))"
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_status CHECK (status IN ('pending', 'accepted', 'rejected', 'dismissed'))"
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_sender CHECK ((message_type = 'system' AND sender_id IS NULL) OR (message_type <> 'system' AND sender_id IS NOT NULL))"
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_friendship CHECK ((message_type = 'friend_request' AND friendship_id IS NOT NULL) OR (message_type <> 'friend_request' AND friendship_id IS NULL))"
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_schedule_item CHECK ((message_type = 'calendar' AND schedule_item_id IS NOT NULL) OR (message_type <> 'calendar' AND schedule_item_id IS NULL))"
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_group CHECK ((message_type = 'group' AND group_id IS NOT NULL) OR (message_type <> 'group' AND group_id IS NULL))"
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_system_fields CHECK ((message_type = 'system' AND friendship_id IS NULL AND schedule_item_id IS NULL AND group_id IS NULL) OR (message_type <> 'system' AND (friendship_id IS NOT NULL OR schedule_item_id IS NOT NULL OR group_id IS NOT NULL)))"
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_inbox_messages_recipient_id",
|
|
"inbox_messages",
|
|
"users",
|
|
["recipient_id"],
|
|
["id"],
|
|
referent_schema="auth",
|
|
ondelete="CASCADE",
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_inbox_messages_sender_id",
|
|
"inbox_messages",
|
|
"users",
|
|
["sender_id"],
|
|
["id"],
|
|
referent_schema="auth",
|
|
ondelete="SET NULL",
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_inbox_messages_friendship_id",
|
|
"inbox_messages",
|
|
"friendships",
|
|
["friendship_id"],
|
|
["id"],
|
|
ondelete="CASCADE",
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_inbox_messages_schedule_item_id",
|
|
"inbox_messages",
|
|
"schedule_items",
|
|
["schedule_item_id"],
|
|
["id"],
|
|
ondelete="CASCADE",
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_inbox_messages_group_id",
|
|
"inbox_messages",
|
|
"groups",
|
|
["group_id"],
|
|
["id"],
|
|
ondelete="CASCADE",
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_inbox_messages_created_by",
|
|
"inbox_messages",
|
|
"users",
|
|
["created_by"],
|
|
["id"],
|
|
referent_schema="auth",
|
|
ondelete="SET NULL",
|
|
)
|
|
_enable_rls("inbox_messages")
|
|
|
|
|
|
def downgrade() -> None:
|
|
_drop_rls("inbox_messages")
|
|
op.drop_constraint(
|
|
"fk_inbox_messages_created_by", "inbox_messages", type_="foreignkey"
|
|
)
|
|
op.drop_constraint(
|
|
"fk_inbox_messages_group_id", "inbox_messages", type_="foreignkey"
|
|
)
|
|
op.drop_constraint(
|
|
"fk_inbox_messages_schedule_item_id", "inbox_messages", type_="foreignkey"
|
|
)
|
|
op.drop_constraint(
|
|
"fk_inbox_messages_friendship_id", "inbox_messages", type_="foreignkey"
|
|
)
|
|
op.drop_constraint(
|
|
"fk_inbox_messages_sender_id", "inbox_messages", type_="foreignkey"
|
|
)
|
|
op.drop_constraint(
|
|
"fk_inbox_messages_recipient_id", "inbox_messages", type_="foreignkey"
|
|
)
|
|
op.drop_table("inbox_messages")
|
|
|
|
_drop_rls("todo_sources")
|
|
op.drop_constraint(
|
|
"fk_todo_sources_schedule_item_id", "todo_sources", type_="foreignkey"
|
|
)
|
|
op.drop_constraint("fk_todo_sources_todo_id", "todo_sources", type_="foreignkey")
|
|
op.drop_table("todo_sources")
|
|
|
|
_drop_rls("todos")
|
|
op.drop_constraint("fk_todos_created_by", "todos", type_="foreignkey")
|
|
op.drop_constraint("fk_todos_owner_id", "todos", type_="foreignkey")
|
|
op.drop_table("todos")
|
|
|
|
_drop_rls("schedule_subscriptions")
|
|
op.drop_constraint(
|
|
"fk_schedule_subscriptions_created_by",
|
|
"schedule_subscriptions",
|
|
type_="foreignkey",
|
|
)
|
|
op.drop_constraint(
|
|
"fk_schedule_subscriptions_subscriber_id",
|
|
"schedule_subscriptions",
|
|
type_="foreignkey",
|
|
)
|
|
op.drop_constraint(
|
|
"fk_schedule_subscriptions_item_id",
|
|
"schedule_subscriptions",
|
|
type_="foreignkey",
|
|
)
|
|
op.drop_table("schedule_subscriptions")
|
|
|
|
_drop_rls("schedule_items")
|
|
op.drop_constraint(
|
|
"fk_schedule_items_created_by", "schedule_items", type_="foreignkey"
|
|
)
|
|
op.drop_constraint(
|
|
"fk_schedule_items_owner_id", "schedule_items", type_="foreignkey"
|
|
)
|
|
op.drop_table("schedule_items")
|
|
|
|
|
|
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")
|