""" SQLAlchemy ORM models for CodyBuddy. """ import uuid from datetime import datetime, timezone from typing import Optional from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import Mapped, mapped_column from db.database import Base def _utcnow() -> datetime: return datetime.now(timezone.utc) class User(Base): __tablename__ = "users" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) name: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) hashed_password: Mapped[Optional[str]] = mapped_column(Text, nullable=True) google_id: Mapped[Optional[str]] = mapped_column(String(255), unique=True, nullable=True, index=True) # ponytail: allowlist-driven role; "user" by default, "SUPER_ADMIN" for allowlisted emails. # Existing rows on Neon need scripts/migrate_add_role.sql (create_all won't ALTER). role: Mapped[str] = mapped_column(String, default="user", server_default="user", nullable=False) # ponytail: GitHub PAT stored ENCRYPTED (Fernet via auth/crypto.py) — never plaintext. # github_login is cached alongside so /me doesn't call GitHub on every request. # Existing Neon rows need scripts/migrate_add_github_token.sql (create_all won't ALTER). github_token_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True) github_login: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, nullable=False) def __repr__(self) -> str: return f"" class Project(Base): __tablename__ = "projects" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) session_id: Mapped[str] = mapped_column(String, nullable=False, index=True) user_id: Mapped[Optional[uuid.UUID]] = mapped_column( UUID(as_uuid=True), ForeignKey("users.id"), nullable=True, index=True ) title: Mapped[str] = mapped_column(String, nullable=False) brief: Mapped[str] = mapped_column(Text, nullable=False) files: Mapped[dict] = mapped_column(JSONB, nullable=False) # ponytail: which framework this app was generated as ("react" | "static" | "nextjs"). # Existing Neon rows need scripts/migrate_add_framework.sql (create_all won't ALTER). framework: Mapped[str] = mapped_column( String, nullable=False, default="react", server_default="react" ) # ponytail: session-history columns; existing Neon rows need # scripts/migrate_add_session_history.sql (create_all won't ALTER). chat: Mapped[list] = mapped_column(JSONB, nullable=False, server_default="[]") oncall_log: Mapped[list] = mapped_column(JSONB, nullable=False, server_default="[]") # ponytail: version history — a capped list of prior file snapshots so a user # can revert when a change (OnCall/enhance/edit) messes the app up. Each entry: # {id, at, source, label, files}. Existing Neon rows need # scripts/migrate_add_versions.sql (create_all won't ALTER). versions: Mapped[list] = mapped_column(JSONB, nullable=False, server_default="[]") created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=_utcnow, nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=_utcnow, onupdate=_utcnow, nullable=False ) # ponytail: index=True on the column handles ix_projects_session_id; no __table_args__ needed class AiCallLog(Base): """Observability — one row per LLM call across all agents. Best-effort (writes never break a build). SUPER_ADMIN-only viewer reads recent rows.""" __tablename__ = "ai_call_logs" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, nullable=False, index=True) agent: Mapped[str] = mapped_column(String, nullable=False, default="") kind: Mapped[str] = mapped_column(String, nullable=False, default="chat") provider: Mapped[str] = mapped_column(String, nullable=False, default="") model: Mapped[str] = mapped_column(String, nullable=False, default="") latency_ms: Mapped[int] = mapped_column(Integer, nullable=False, default=0) ok: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) error: Mapped[str] = mapped_column(Text, nullable=False, default="") prompt: Mapped[str] = mapped_column(Text, nullable=False, default="") response: Mapped[str] = mapped_column(Text, nullable=False, default="")