- Add shared/ module: config, db, models, schemas, redis utilities - Add Auth Service (services/auth/): register, login, refresh, me, ForwardAuth /verify endpoint for Traefik - Add Traefik config: ACME/Cloudflare DNS-01, dynamic routing, ForwardAuth middleware, sticky sessions for WS Gateway - Add service scaffolds: ws-gateway, chat, batch-agent, billing (READMEs) - Add redis>=5.0.0 to requirements.txt - Monolith app/ is untouched — strangler fig migration
456 lines
20 KiB
Python
456 lines
20 KiB
Python
"""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()
|
|
)
|