Compare commits

...

2 Commits

7 changed files with 358 additions and 2 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import create_async_engine
from app.config import settings from app.config import settings
from app.database import Base from app.database import Base
from app.models import user, session, world, asset # noqa: F401 from app.models import user, session, world, asset, friendship, follow, block # noqa: F401
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
@@ -0,0 +1,136 @@
"""social graph: friendships, follows, blocks
Revision ID: 0002_social_graph
Revises: 0001_initial
Create Date: 2026-08-18 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "0002_social_graph"
down_revision: Union[str, None] = "0001_initial"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Create friendships, follows, and blocks tables."""
op.create_table(
"friendships",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("requester_id", sa.Integer(), nullable=False),
sa.Column("addressee_id", sa.Integer(), nullable=False),
sa.Column("status", sa.String(length=20), server_default="pending", nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
sa.CheckConstraint(
"status IN ('pending', 'accepted')",
name="ck_friendships_status",
),
sa.CheckConstraint(
"requester_id <> addressee_id",
name="ck_friendships_no_self",
),
sa.ForeignKeyConstraint(
["addressee_id"],
["users.id"],
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["requester_id"],
["users.id"],
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"requester_id",
"addressee_id",
name="uq_friendships_requester_addressee",
),
)
op.create_index(op.f("ix_friendships_id"), "friendships", ["id"], unique=False)
op.create_index(op.f("ix_friendships_addressee_id"), "friendships", ["addressee_id"], unique=False)
op.create_index(op.f("ix_friendships_requester_id"), "friendships", ["requester_id"], unique=False)
op.create_table(
"follows",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("follower_id", sa.Integer(), nullable=False),
sa.Column("followed_id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.CheckConstraint(
"follower_id <> followed_id",
name="ck_follows_no_self",
),
sa.ForeignKeyConstraint(
["followed_id"],
["users.id"],
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["follower_id"],
["users.id"],
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"follower_id",
"followed_id",
name="uq_follows_follower_followed",
),
)
op.create_index(op.f("ix_follows_id"), "follows", ["id"], unique=False)
op.create_index(op.f("ix_follows_followed_id"), "follows", ["followed_id"], unique=False)
op.create_index(op.f("ix_follows_follower_id"), "follows", ["follower_id"], unique=False)
op.create_table(
"blocks",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("blocker_id", sa.Integer(), nullable=False),
sa.Column("blocked_id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.CheckConstraint(
"blocker_id <> blocked_id",
name="ck_blocks_no_self",
),
sa.ForeignKeyConstraint(
["blocked_id"],
["users.id"],
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["blocker_id"],
["users.id"],
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"blocker_id",
"blocked_id",
name="uq_blocks_blocker_blocked",
),
)
op.create_index(op.f("ix_blocks_id"), "blocks", ["id"], unique=False)
op.create_index(op.f("ix_blocks_blocked_id"), "blocks", ["blocked_id"], unique=False)
op.create_index(op.f("ix_blocks_blocker_id"), "blocks", ["blocker_id"], unique=False)
def downgrade() -> None:
"""Drop friendships, follows, and blocks tables."""
op.drop_index(op.f("ix_blocks_blocker_id"), table_name="blocks")
op.drop_index(op.f("ix_blocks_blocked_id"), table_name="blocks")
op.drop_index(op.f("ix_blocks_id"), table_name="blocks")
op.drop_table("blocks")
op.drop_index(op.f("ix_follows_follower_id"), table_name="follows")
op.drop_index(op.f("ix_follows_followed_id"), table_name="follows")
op.drop_index(op.f("ix_follows_id"), table_name="follows")
op.drop_table("follows")
op.drop_index(op.f("ix_friendships_requester_id"), table_name="friendships")
op.drop_index(op.f("ix_friendships_addressee_id"), table_name="friendships")
op.drop_index(op.f("ix_friendships_id"), table_name="friendships")
op.drop_table("friendships")
+13 -1
View File
@@ -2,5 +2,17 @@ from app.models.user import User
from app.models.session import Session from app.models.session import Session
from app.models.world import World from app.models.world import World
from app.models.asset import Asset from app.models.asset import Asset
from app.models.friendship import Friendship, FriendshipStatus
from app.models.follow import Follow
from app.models.block import Block
__all__ = ["User", "Session", "World", "Asset"] __all__ = [
"User",
"Session",
"World",
"Asset",
"Friendship",
"FriendshipStatus",
"Follow",
"Block",
]
+48
View File
@@ -0,0 +1,48 @@
"""Block model: a hard deny that also hides content and prevents DMs."""
from sqlalchemy import CheckConstraint, Column, DateTime, ForeignKey, Integer, UniqueConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class Block(Base):
__tablename__ = "blocks"
__table_args__ = (
UniqueConstraint(
"blocker_id",
"blocked_id",
name="uq_blocks_pair",
),
CheckConstraint(
"blocker_id <> blocked_id",
name="ck_blocks_no_self",
),
)
id = Column(Integer, primary_key=True, index=True)
blocker_id = Column(
Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
blocked_id = Column(
Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
created_at = Column(DateTime(timezone=True), server_default=func.now())
blocker = relationship(
"User",
foreign_keys=[blocker_id],
back_populates="blocks_issued",
)
blocked = relationship(
"User",
foreign_keys=[blocked_id],
back_populates="blocks_received",
)
+48
View File
@@ -0,0 +1,48 @@
"""Follow model: a one-way relationship requiring no approval."""
from sqlalchemy import CheckConstraint, Column, DateTime, ForeignKey, Integer, UniqueConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class Follow(Base):
__tablename__ = "follows"
__table_args__ = (
UniqueConstraint(
"follower_id",
"followed_id",
name="uq_follows_pair",
),
CheckConstraint(
"follower_id <> followed_id",
name="ck_follows_no_self",
),
)
id = Column(Integer, primary_key=True, index=True)
follower_id = Column(
Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
followed_id = Column(
Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
created_at = Column(DateTime(timezone=True), server_default=func.now())
follower = relationship(
"User",
foreign_keys=[follower_id],
back_populates="following",
)
followed = relationship(
"User",
foreign_keys=[followed_id],
back_populates="followers",
)
+74
View File
@@ -0,0 +1,74 @@
"""Friendship model: a one-directional request that becomes mutual on accept."""
from sqlalchemy import (
CheckConstraint,
Column,
DateTime,
ForeignKey,
Integer,
String,
UniqueConstraint,
)
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class FriendshipStatus:
"""String values for the friendship.status column."""
PENDING = "pending"
ACCEPTED = "accepted"
class Friendship(Base):
__tablename__ = "friendships"
__table_args__ = (
UniqueConstraint(
"requester_id",
"addressee_id",
name="uq_friendships_pair",
),
CheckConstraint(
"requester_id <> addressee_id",
name="ck_friendships_no_self",
),
CheckConstraint(
"status IN ('pending', 'accepted')",
name="ck_friendships_status",
),
)
id = Column(Integer, primary_key=True, index=True)
requester_id = Column(
Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
addressee_id = Column(
Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
status = Column(
String(20),
nullable=False,
default=FriendshipStatus.PENDING,
server_default=FriendshipStatus.PENDING,
)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
requester = relationship(
"User",
foreign_keys=[requester_id],
back_populates="sent_friend_requests",
)
addressee = relationship(
"User",
foreign_keys=[addressee_id],
back_populates="received_friend_requests",
)
+38
View File
@@ -19,3 +19,41 @@ class User(Base):
sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan") sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan")
worlds = relationship("World", back_populates="owner") worlds = relationship("World", back_populates="owner")
assets = relationship("Asset", back_populates="owner") assets = relationship("Asset", back_populates="owner")
# Social graph
sent_friend_requests = relationship(
"Friendship",
foreign_keys="Friendship.requester_id",
back_populates="requester",
cascade="all, delete-orphan",
)
received_friend_requests = relationship(
"Friendship",
foreign_keys="Friendship.addressee_id",
back_populates="addressee",
cascade="all, delete-orphan",
)
following = relationship(
"Follow",
foreign_keys="Follow.follower_id",
back_populates="follower",
cascade="all, delete-orphan",
)
followers = relationship(
"Follow",
foreign_keys="Follow.followed_id",
back_populates="followed",
cascade="all, delete-orphan",
)
blocks_issued = relationship(
"Block",
foreign_keys="Block.blocker_id",
back_populates="blocker",
cascade="all, delete-orphan",
)
blocks_received = relationship(
"Block",
foreign_keys="Block.blocked_id",
back_populates="blocked",
cascade="all, delete-orphan",
)