Compare commits

..

2 Commits

8 changed files with 78 additions and 352 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import create_async_engine
from app.config import settings
from app.database import Base
from app.models import user, session, world, asset, friendship, follow, block # noqa: F401
from app.models import user, session, world, asset, wallet # noqa: F401
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
@@ -1,136 +0,0 @@
"""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")
+4 -7
View File
@@ -2,17 +2,14 @@ from app.models.user import User
from app.models.session import Session
from app.models.world import World
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
from app.models.wallet import Transaction, TransactionType, Wallet
__all__ = [
"User",
"Session",
"World",
"Asset",
"Friendship",
"FriendshipStatus",
"Follow",
"Block",
"Wallet",
"Transaction",
"TransactionType",
]
-48
View File
@@ -1,48 +0,0 @@
"""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
@@ -1,48 +0,0 @@
"""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
@@ -1,74 +0,0 @@
"""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,41 +19,3 @@ class User(Base):
sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan")
worlds = relationship("World", 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",
)
+73
View File
@@ -0,0 +1,73 @@
"""Wallet and transaction ledger models."""
import enum
from sqlalchemy import (
BigInteger,
CheckConstraint,
Column,
DateTime,
ForeignKey,
Integer,
Text,
)
from sqlalchemy.dialects.postgresql import ENUM, JSONB
from sqlalchemy.sql import func
from app.database import Base
class TransactionType(str, enum.Enum):
CREDIT_PURCHASE = "credit_purchase"
MARKETPLACE_PURCHASE = "marketplace_purchase"
MARKETPLACE_SALE = "marketplace_sale"
ESCROW_RELEASE = "escrow_release"
PAYOUT = "payout"
REFUND = "refund"
ADMIN_ADJUSTMENT = "admin_adjustment"
class Wallet(Base):
__tablename__ = "wallets"
__table_args__ = (
CheckConstraint("balance >= 0", name="ck_wallets_balance_nonnegative"),
)
id = Column(BigInteger, primary_key=True, index=True)
user_id = Column(
Integer,
ForeignKey("users.id", ondelete="CASCADE"),
unique=True,
index=True,
nullable=False,
)
balance = Column(BigInteger, nullable=False, default=0, server_default="0")
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class Transaction(Base):
__tablename__ = "transactions"
id = Column(BigInteger, primary_key=True, index=True)
wallet_id = Column(
BigInteger, ForeignKey("wallets.id"), nullable=False
)
type = Column(
ENUM(
TransactionType,
name="transaction_type",
create_type=False,
values_callable=lambda enum_cls: [member.value for member in enum_cls],
),
nullable=False,
)
amount = Column(BigInteger, nullable=False)
balance_after = Column(BigInteger, nullable=False)
reference_type = Column(Text, nullable=True)
reference_id = Column(Text, nullable=True)
idempotency_key = Column(Text, unique=True, index=True, nullable=True)
metadata_json = Column(JSONB, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())