fractafrag/services/api/app/models/models.py
John Lightner 365c033e0e Fix Docker Compose startup issues
- Rename EngagementEvent.metadata → event_metadata (SQLAlchemy reserved name)
- Replace passlib with direct bcrypt usage (passlib incompatible with bcrypt 5.0)
- Fix renderer Dockerfile: npm ci → npm install (no lockfile)
- Fix frontend Dockerfile: single-stage, skip tsc for builds
- Remove deprecated 'version' key from docker-compose.yml
- Add docker-compose.dev.yml for data-stores-only local dev
- Add start_period to API healthcheck for startup grace
2026-03-24 21:06:01 -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)
event_metadata = Column("metadata", 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)