"""SQLAlchemy ORM models for all persistent tables. Centralized here so that Alembic migrations and all services share the same model definitions. Each service only queries the tables it owns. Ownership: Auth Service → users, refresh_tokens, subscriptions Chat Service → memory_core, memory_associative, memory_episodic, memory_proactive Batch Agent → local_agent_configs, cloud_agent_configs, agent_run_logs Billing Service → subscriptions (shared write with Auth) (excluded MVP) → storage_records, backup_metadata, plugins, plugin_*, revenue_events """ from __future__ import annotations import uuid from datetime import datetime, timezone from sqlalchemy import ( BigInteger, Boolean, DateTime, Enum, Float, ForeignKey, Integer, JSON, String, Text, UniqueConstraint, Uuid, func, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from shared.db import Base # ── Helpers ────────────────────────────────────────────────────────────── def _uuid() -> str: return str(uuid.uuid4()) def _now() -> datetime: return datetime.now(timezone.utc) # ── Enum types ──────────────────────────────────────────────────────────── TierEnum = Enum("free", "pro", "power", "team", name="billing_tier") PluginStatusEnum = Enum("pending_review", "approved", "rejected", name="plugin_status") ReviewDecisionEnum = Enum("approved", "rejected", name="review_decision") AgentTypeEnum = Enum("local", "cloud", name="agent_type") AgentStatusEnum = Enum("running", "success", "error", "partial", name="agent_run_status") CloudProviderEnum = Enum("gmail", "teams", "outlook", name="cloud_provider") # ── Auth models ─────────────────────────────────────────────────────────── class User(Base): __tablename__ = "users" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) name: Mapped[str | None] = mapped_column(String(100), nullable=True) surname: Mapped[str | None] = mapped_column(String(100), nullable=True) password_hash: Mapped[str] = mapped_column(String(255), nullable=False) tier: Mapped[str] = mapped_column(TierEnum, nullable=False, default="free") stripe_customer_id: Mapped[str | None] = mapped_column(String(255), nullable=True) encryption_key: Mapped[str | None] = mapped_column(String(64), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) refresh_tokens: Mapped[list[RefreshToken]] = relationship( back_populates="user", cascade="all, delete-orphan" ) subscription: Mapped[Subscription | None] = relationship( back_populates="user", uselist=False, cascade="all, delete-orphan" ) class RefreshToken(Base): __tablename__ = "refresh_tokens" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) token_hash: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) user: Mapped[User] = relationship(back_populates="refresh_tokens") class Subscription(Base): __tablename__ = "subscriptions" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True, index=True ) stripe_subscription_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True) tier: Mapped[str] = mapped_column(TierEnum, nullable=False, default="free") status: Mapped[str] = mapped_column(String(50), nullable=False, default="free") current_period_end: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) user: Mapped[User] = relationship(back_populates="subscription") # ── Storage models (excluded from MVP, kept for Alembic) ────────────── class StorageRecord(Base): __tablename__ = "storage_records" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) table_name: Mapped[str] = mapped_column(String(100), nullable=False) s3_key: Mapped[str] = mapped_column(String(500), nullable=False) checksum: Mapped[str] = mapped_column(String(64), nullable=False) size_bytes: Mapped[int] = mapped_column(Integer, nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) class BackupMetadata(Base): __tablename__ = "backup_metadata" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) s3_key: Mapped[str] = mapped_column(String(500), nullable=False) version: Mapped[int] = mapped_column(Integer, nullable=False) timestamp: Mapped[int] = mapped_column(BigInteger, nullable=False) checksum: Mapped[str] = mapped_column(String(64), nullable=False) size_bytes: Mapped[int] = mapped_column(Integer, nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) # ── Plugin models (excluded from MVP, kept for Alembic) ─────────────── class Plugin(Base): __tablename__ = "plugins" id: Mapped[str] = mapped_column(String(255), primary_key=True) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str] = mapped_column(Text, nullable=False, default="") version: Mapped[str] = mapped_column(String(50), nullable=False, default="1.0.0") author_id: Mapped[str | None] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) author_name: Mapped[str] = mapped_column(String(255), nullable=False, default="") category: Mapped[str] = mapped_column(String(100), nullable=False, default="") price_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0) permissions: Mapped[str] = mapped_column(Text, nullable=False, default="[]") status: Mapped[str] = mapped_column(PluginStatusEnum, nullable=False, default="pending_review") s3_package_key: Mapped[str | None] = mapped_column(String(500), nullable=True) install_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) avg_rating: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) rejection_reason: Mapped[str | None] = mapped_column(Text, nullable=True) submitted_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) installations: Mapped[list[PluginInstallation]] = relationship( back_populates="plugin", cascade="all, delete-orphan" ) reviews: Mapped[list[PluginReview]] = relationship( back_populates="plugin", cascade="all, delete-orphan" ) revenue_events: Mapped[list[RevenueEvent]] = relationship( back_populates="plugin", cascade="all, delete-orphan" ) class PluginInstallation(Base): __tablename__ = "plugin_installations" __table_args__ = (UniqueConstraint("plugin_id", "user_id", name="uq_plugin_user"),) id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) plugin_id: Mapped[str] = mapped_column( String(255), ForeignKey("plugins.id", ondelete="CASCADE"), nullable=False, index=True ) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) installed_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) plugin: Mapped[Plugin] = relationship(back_populates="installations") class PluginReview(Base): __tablename__ = "plugin_reviews" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) plugin_id: Mapped[str] = mapped_column( String(255), ForeignKey("plugins.id", ondelete="CASCADE"), nullable=False, index=True ) reviewer_id: Mapped[str | None] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) decision: Mapped[str] = mapped_column(ReviewDecisionEnum, nullable=False) notes: Mapped[str | None] = mapped_column(Text, nullable=True) reviewed_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) plugin: Mapped[Plugin] = relationship(back_populates="reviews") class RevenueEvent(Base): __tablename__ = "revenue_events" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) plugin_id: Mapped[str] = mapped_column( String(255), ForeignKey("plugins.id", ondelete="CASCADE"), nullable=False, index=True ) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) amount_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0) developer_share_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0) stripe_transfer_id: Mapped[str | None] = mapped_column(String(255), nullable=True) paid_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) plugin: Mapped[Plugin] = relationship(back_populates="revenue_events") # ── Agent models ────────────────────────────────────────────────────────── class LocalAgentConfig(Base): __tablename__ = "local_agent_configs" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) device_id: Mapped[str] = mapped_column(String(255), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) directory_paths: Mapped[list] = mapped_column(JSON, nullable=False, default=list) data_types: Mapped[list] = mapped_column(JSON, nullable=False, default=list) prompt_template: Mapped[str] = mapped_column(Text, nullable=False, default="") file_extensions: Mapped[list] = mapped_column(JSON, nullable=False, default=list) schedule_cron: Mapped[str] = mapped_column(String(100), nullable=False, default="0 */6 * * *") enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) last_run_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) run_logs: Mapped[list[AgentRunLog]] = relationship( back_populates="local_agent", primaryjoin="and_(AgentRunLog.agent_id == LocalAgentConfig.id, AgentRunLog.agent_type == 'local')", foreign_keys="AgentRunLog.agent_id", cascade="all, delete-orphan", overlaps="run_logs,cloud_agent", ) class CloudAgentConfig(Base): __tablename__ = "cloud_agent_configs" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) provider: Mapped[str] = mapped_column(CloudProviderEnum, nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) data_types: Mapped[list] = mapped_column(JSON, nullable=False, default=list) prompt_template: Mapped[str] = mapped_column(Text, nullable=False, default="") oauth_token_encrypted: Mapped[str | None] = mapped_column(Text, nullable=True) filter_config: Mapped[dict | None] = mapped_column(JSON, nullable=True) schedule_cron: Mapped[str] = mapped_column(String(100), nullable=False, default="0 */6 * * *") enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) last_run_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) run_logs: Mapped[list[AgentRunLog]] = relationship( back_populates="cloud_agent", primaryjoin="and_(AgentRunLog.agent_id == CloudAgentConfig.id, AgentRunLog.agent_type == 'cloud')", foreign_keys="AgentRunLog.agent_id", cascade="all, delete-orphan", overlaps="run_logs,local_agent", ) class AgentRunLog(Base): __tablename__ = "agent_run_logs" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) agent_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True) agent_type: Mapped[str] = mapped_column(AgentTypeEnum, nullable=False) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) status: Mapped[str] = mapped_column(AgentStatusEnum, nullable=False, default="running") items_processed: Mapped[int] = mapped_column(Integer, nullable=False, default=0) items_created: Mapped[int] = mapped_column(Integer, nullable=False, default=0) errors: Mapped[list | None] = mapped_column(JSON, nullable=True) started_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) local_agent: Mapped[LocalAgentConfig | None] = relationship( back_populates="run_logs", primaryjoin="and_(AgentRunLog.agent_id == LocalAgentConfig.id, AgentRunLog.agent_type == 'local')", foreign_keys="AgentRunLog.agent_id", overlaps="run_logs,cloud_agent", ) cloud_agent: Mapped[CloudAgentConfig | None] = relationship( back_populates="run_logs", primaryjoin="and_(AgentRunLog.agent_id == CloudAgentConfig.id, AgentRunLog.agent_type == 'cloud')", foreign_keys="AgentRunLog.agent_id", overlaps="run_logs,local_agent", ) # ── Memory models ───────────────────────────────────────────────────────── class MemoryCore(Base): """Per-user persistent key/value preferences, encrypted at rest.""" __tablename__ = "memory_core" id: Mapped[str] = mapped_column(Uuid(as_uuid=False), primary_key=True, default=_uuid) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, ) key: Mapped[str] = mapped_column(String(255), nullable=False) value_encrypted: Mapped[str] = mapped_column(Text, nullable=False) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) class MemoryAssociative(Base): """Per-user semantic memory: encrypted content + pgvector embedding.""" __tablename__ = "memory_associative" id: Mapped[str] = mapped_column(Uuid(as_uuid=False), primary_key=True, default=_uuid) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, ) content_encrypted: Mapped[str] = mapped_column(Text, nullable=False) embedding: Mapped[list | None] = mapped_column(JSON, nullable=True) entity_type: Mapped[str | None] = mapped_column(String(100), nullable=True) entity_id: Mapped[str | None] = mapped_column(String(255), nullable=True) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) class MemoryEpisodic(Base): """Per-user session summaries, encrypted at rest.""" __tablename__ = "memory_episodic" id: Mapped[str] = mapped_column(Uuid(as_uuid=False), primary_key=True, default=_uuid) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, ) summary_encrypted: Mapped[str] = mapped_column(Text, nullable=False) session_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) class MemoryProactive(Base): """Per-user inferred behavioral patterns, encrypted at rest.""" __tablename__ = "memory_proactive" id: Mapped[str] = mapped_column(Uuid(as_uuid=False), primary_key=True, default=_uuid) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, ) pattern_encrypted: Mapped[str] = mapped_column(Text, nullable=False) confidence: Mapped[float] = mapped_column(Float, nullable=False, default=0.5) source: Mapped[str] = mapped_column(String(50), nullable=False, default="inferred") created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() )