af85048deb
New avatar_loadouts table (one row per user, loadout stored as a single JSONB document matching the client's AvatarLoadout.to_json() shape) and GET/PUT /api/avatar/loadout. The server has no copy of the asset catalog, so it validates only the envelope (JSON object, non-empty base_body, object slots, size cap) -- which-asset-in-which-slot validation stays client-side. New accounts get a server default (base_human_male_01, no slots) with is_default:true so the client can tell "never customised" from "customised back to base". - models/avatar_loadout.py + 0004 migration + User.avatar_loadout relation - routers/avatar.py wired into main.py - tests/test_avatar_loadout.py (7 tests: default, save/read, replace, validation, auth) -- full suite 68 passed - lifted the duplicated _override_db fixture from 3 test files into conftest.py (new test files need it too) - docs/architecture/avatar.md §9a: the persistence design Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
42 lines
1.5 KiB
Python
42 lines
1.5 KiB
Python
from sqlalchemy import Column, Integer, ForeignKey, DateTime
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.orm import relationship
|
|
from sqlalchemy.sql import func
|
|
|
|
from app.database import Base
|
|
|
|
|
|
# The default a brand-new account gets before they touch the customization
|
|
# screen. Kept in sync with assets/avatars/catalog.json (the base_body id) and
|
|
# with the client's AvatarLoadout.to_json() shape.
|
|
DEFAULT_LOADOUT: dict = {
|
|
"base_body": "base_human_male_01",
|
|
"slots": {},
|
|
}
|
|
|
|
|
|
class AvatarLoadout(Base):
|
|
"""A user's equipped avatar composition.
|
|
|
|
One row per user. The whole loadout is stored as a single JSON document
|
|
matching the client's ``AvatarLoadout.to_json()`` shape
|
|
(``{"base_body": str, "slots": {...}, "skin_tone"?: {...}}``) rather than
|
|
exploded into columns -- the server has no copy of the asset catalog, so
|
|
deep validation (does this asset exist, is it valid for this slot) stays
|
|
on the client. The server only enforces the envelope: valid JSON object
|
|
with a non-empty ``base_body``.
|
|
"""
|
|
|
|
__tablename__ = "avatar_loadouts"
|
|
|
|
user_id = Column(
|
|
Integer,
|
|
ForeignKey("users.id", ondelete="CASCADE"),
|
|
primary_key=True,
|
|
)
|
|
data = Column(JSONB, nullable=False)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
|
|
|
user = relationship("User", back_populates="avatar_loadout")
|