feat(discovery): world discovery metadata and world_visits model with migration 0015

This commit is contained in:
2026-09-15 20:56:01 +00:00
parent f8b1fc4ea7
commit e3188b6123
7 changed files with 266 additions and 4 deletions
+2
View File
@@ -15,6 +15,7 @@ from app.models.marketplace import (
CatalogItem, MarketplaceListing, Purchase, CreatorBalance,
ItemType, ListingStatus, PurchaseStatus,
)
from app.models.world_visit import WorldVisit
__all__ = [
"User", "Session", "World", "Asset", "AvatarLoadout",
@@ -26,4 +27,5 @@ __all__ = [
"Wallet", "Transaction", "TransactionType",
"CatalogItem", "MarketplaceListing", "Purchase", "CreatorBalance",
"ItemType", "ListingStatus", "PurchaseStatus",
"WorldVisit",
]
+12 -4
View File
@@ -1,5 +1,5 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Index
from sqlalchemy.dialects.postgresql import ENUM, ARRAY
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import enum
@@ -29,11 +29,19 @@ class World(Base):
),
default=WorldVisibility.PUBLIC,
)
category = Column(String(100), nullable=True, index=True)
tags = Column(ARRAY(String), nullable=True, server_default="{}")
play_count = Column(Integer, nullable=False, server_default="0", index=True)
last_played_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
__table_args__ = (
Index("ix_worlds_tags", "tags", postgresql_using="gin"),
)
owner = relationship("User", back_populates="worlds")
assets = relationship("Asset", back_populates="world", cascade="all, delete-orphan")
# world_content.py / world_persistence_impl.md §4.1. Added here (only)
# so PC1/PC2 code depending on this module is otherwise untouched.
rooms = relationship("WorldRoom", back_populates="world", cascade="all, delete-orphan")
visits = relationship("WorldVisit", back_populates="world", cascade="all, delete-orphan")
+27
View File
@@ -0,0 +1,27 @@
"""World visit tracking model for discovery analytics.
docs/planning/discovery.md (task b377353b8167).
"""
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Index
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class WorldVisit(Base):
__tablename__ = "world_visits"
id = Column(Integer, primary_key=True, index=True)
world_id = Column(Integer, ForeignKey("worlds.id", ondelete="CASCADE"), nullable=False, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
anonymous_token = Column(String(255), nullable=True)
visited_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
__table_args__ = (
Index("ix_world_visits_world_visited_at", "world_id", "visited_at"),
)
world = relationship("World", back_populates="visits")
user = relationship("User")
+12
View File
@@ -27,6 +27,8 @@ class WorldCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
description: str | None = None
visibility: WorldVisibility = WorldVisibility.PUBLIC
category: str | None = None
tags: list[str] | None = None
class WorldUpdate(BaseModel):
@@ -34,6 +36,8 @@ class WorldUpdate(BaseModel):
name: str | None = Field(None, min_length=1, max_length=255)
description: str | None = None
visibility: WorldVisibility | None = None
category: str | None = None
tags: list[str] | None = None
class WorldResponse(BaseModel):
@@ -42,12 +46,17 @@ class WorldResponse(BaseModel):
name: str
description: str | None
visibility: WorldVisibility
category: str | None = None
tags: list[str] | None = None
play_count: int = 0
last_played_at: datetime | None = None
owner_id: int
created_at: datetime | None = None
model_config = {"from_attributes": True}
# --- Endpoints ---
@router.get("/", response_model=list[WorldResponse])
@@ -159,7 +168,10 @@ async def create_world(
description=world_data.description,
owner_id=current_user.id,
visibility=world_data.visibility,
category=world_data.category,
tags=world_data.tags or [],
)
db.add(world)
await db.flush() # assigns world.id, needed for the room's FK
await WorldStore(db).get_or_create_main_room(world.id)