fractafrag/services/api/app/models/models.py
John Lightner 05d39fdda8 M0: Foundation scaffold — Docker Compose, DB schema, FastAPI app, all service stubs
Track A (Infrastructure & Data Layer):
- docker-compose.yml with all 7 services (nginx, frontend, api, mcp, renderer, worker, postgres, redis)
- docker-compose.override.yml for local dev (hot reload, port exposure)
- PostgreSQL init.sql with full schema (15 tables, pgvector indexes, creator economy stubs)
- .env.example with all required environment variables

Track A+B (API Layer):
- FastAPI app with 10 routers (auth, shaders, feed, votes, generate, desires, users, payments, mcp_keys, health)
- SQLAlchemy ORM models for all 15 tables
- Pydantic schemas for all request/response types
- JWT auth middleware (access + refresh tokens, Redis blocklist)
- Redis rate limiting middleware
- Celery worker config with job stubs (render, embed, generate, feed cache, expire bounties)
- Alembic migration framework

Service stubs:
- MCP server (health endpoint, 501 for all tools)
- Renderer service (Express + Puppeteer scaffold, 501 for /render)
- Frontend (package.json with React/Vite/Three.js/TanStack/Tailwind deps)
- Nginx reverse proxy config (/, /api, /mcp, /renders)

Project:
- DECISIONS.md with 11 recorded architectural decisions
- README.md with architecture overview
- Sample shader seed data (plasma, fractal noise, raymarched sphere)
2026-03-24 20:45:08 -05:00

222 lines
9.9 KiB
Python

"""Fractafrag — SQLAlchemy ORM Models."""
import uuid
from datetime import datetime
from sqlalchemy import (
Column, String, Text, Boolean, Integer, Float, SmallInteger,
ForeignKey, DateTime, UniqueConstraint, Index, CheckConstraint,
)
from sqlalchemy.dialects.postgresql import UUID, JSONB, ARRAY
from pgvector.sqlalchemy import Vector
from sqlalchemy.orm import relationship
from app.database import Base
class User(Base):
__tablename__ = "users"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
username = Column(String, unique=True, nullable=False, index=True)
email = Column(String, unique=True, nullable=False, index=True)
password_hash = Column(String, nullable=False)
role = Column(String, nullable=False, default="user")
trust_tier = Column(String, nullable=False, default="standard")
stripe_customer_id = Column(String, nullable=True)
subscription_tier = Column(String, default="free")
ai_credits_remaining = Column(Integer, default=0)
taste_vector = Column(Vector(512), nullable=True)
# Creator economy stubs
is_verified_creator = Column(Boolean, default=False)
verified_creator_at = Column(DateTime(timezone=True), nullable=True)
stripe_connect_account_id = Column(String, nullable=True)
# Timestamps
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
last_active_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
shaders = relationship("Shader", back_populates="author")
votes = relationship("Vote", back_populates="user")
api_keys = relationship("ApiKey", back_populates="user")
class Shader(Base):
__tablename__ = "shaders"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
author_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
title = Column(String, nullable=False)
description = Column(Text, nullable=True)
glsl_code = Column(Text, nullable=False)
is_public = Column(Boolean, default=True)
is_ai_generated = Column(Boolean, default=False)
ai_provider = Column(String, nullable=True)
thumbnail_url = Column(String, nullable=True)
preview_url = Column(String, nullable=True)
render_status = Column(String, default="pending")
style_vector = Column(Vector(512), nullable=True)
style_metadata = Column(JSONB, nullable=True)
tags = Column(ARRAY(String), default=list)
shader_type = Column(String, default="2d")
forked_from = Column(UUID(as_uuid=True), ForeignKey("shaders.id", ondelete="SET NULL"), nullable=True)
view_count = Column(Integer, default=0)
score = Column(Float, default=0.0)
# Creator economy stubs
access_tier = Column(String, default="open")
source_unlock_price_cents = Column(Integer, nullable=True)
commercial_license_price_cents = Column(Integer, nullable=True)
verified_creator_shader = Column(Boolean, default=False)
# Timestamps
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
author = relationship("User", back_populates="shaders")
votes = relationship("Vote", back_populates="shader")
class Vote(Base):
__tablename__ = "votes"
__table_args__ = (UniqueConstraint("user_id", "shader_id"),)
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
shader_id = Column(UUID(as_uuid=True), ForeignKey("shaders.id", ondelete="CASCADE"), nullable=False)
value = Column(SmallInteger, nullable=False)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
user = relationship("User", back_populates="votes")
shader = relationship("Shader", back_populates="votes")
class EngagementEvent(Base):
__tablename__ = "engagement_events"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
session_id = Column(String, nullable=True)
shader_id = Column(UUID(as_uuid=True), ForeignKey("shaders.id", ondelete="CASCADE"), nullable=False)
event_type = Column(String, nullable=False)
dwell_secs = Column(Float, nullable=True)
metadata = Column(JSONB, nullable=True)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class Desire(Base):
__tablename__ = "desires"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
author_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
prompt_text = Column(Text, nullable=False)
prompt_embedding = Column(Vector(512), nullable=True)
style_hints = Column(JSONB, nullable=True)
tip_amount_cents = Column(Integer, default=0)
status = Column(String, default="open")
heat_score = Column(Float, default=1.0)
fulfilled_by_shader = Column(UUID(as_uuid=True), ForeignKey("shaders.id", ondelete="SET NULL"), nullable=True)
fulfilled_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class DesireCluster(Base):
__tablename__ = "desire_clusters"
cluster_id = Column(UUID(as_uuid=True), primary_key=True)
desire_id = Column(UUID(as_uuid=True), ForeignKey("desires.id", ondelete="CASCADE"), primary_key=True)
similarity = Column(Float)
class BountyTip(Base):
__tablename__ = "bounty_tips"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
desire_id = Column(UUID(as_uuid=True), ForeignKey("desires.id", ondelete="CASCADE"), nullable=False)
tipper_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
amount_cents = Column(Integer, nullable=False)
stripe_payment_intent_id = Column(String, nullable=True)
status = Column(String, default="held")
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class CreatorPayout(Base):
__tablename__ = "creator_payouts"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
creator_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
desire_id = Column(UUID(as_uuid=True), ForeignKey("desires.id", ondelete="SET NULL"), nullable=True)
gross_amount_cents = Column(Integer)
platform_fee_cents = Column(Integer)
net_amount_cents = Column(Integer)
stripe_transfer_id = Column(String, nullable=True)
status = Column(String, default="pending")
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class ApiKey(Base):
__tablename__ = "api_keys"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
key_hash = Column(String, unique=True, nullable=False)
key_prefix = Column(String, nullable=False)
name = Column(String, nullable=True)
trust_tier = Column(String, default="probation")
submissions_approved = Column(Integer, default=0)
rate_limit_per_hour = Column(Integer, default=10)
last_used_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
revoked_at = Column(DateTime(timezone=True), nullable=True)
user = relationship("User", back_populates="api_keys")
class GenerationLog(Base):
__tablename__ = "generation_log"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
shader_id = Column(UUID(as_uuid=True), ForeignKey("shaders.id", ondelete="SET NULL"), nullable=True)
provider = Column(String, nullable=False)
prompt_text = Column(Text, nullable=True)
tokens_used = Column(Integer, nullable=True)
cost_cents = Column(Integer, nullable=True)
success = Column(Boolean, nullable=True)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class Comment(Base):
__tablename__ = "comments"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
shader_id = Column(UUID(as_uuid=True), ForeignKey("shaders.id", ondelete="CASCADE"), nullable=False)
author_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
body = Column(Text, nullable=False)
parent_id = Column(UUID(as_uuid=True), ForeignKey("comments.id", ondelete="CASCADE"), nullable=True)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
# Creator economy stubs (dormant)
class SourceUnlock(Base):
__tablename__ = "source_unlocks"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
shader_id = Column(UUID(as_uuid=True), ForeignKey("shaders.id", ondelete="CASCADE"), nullable=False)
buyer_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
license_type = Column(String, nullable=False)
amount_cents = Column(Integer, nullable=False)
platform_fee_cents = Column(Integer, nullable=False)
stripe_payment_intent_id = Column(String, nullable=True)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class CreatorEngagementSnapshot(Base):
__tablename__ = "creator_engagement_snapshots"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
creator_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
month = Column(DateTime, nullable=False)
total_score = Column(Float, nullable=False)
pool_share = Column(Float, nullable=True)
payout_cents = Column(Integer, nullable=True)
paid_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)