Files
smart-org/apps/api/app/infrastructure/db/models.py
T
mjalmoudy94 8eb631aedf ساختار مالکیت پلتفرم: یک مالک، مدیرانِ ساختهٔ مالک، سازمان‌های مدیریت‌شده
- users.is_platform_owner → users.platform_role (owner|manager|user) با مهاجرت
  d4e5f6a7b8c9 و backfill. ایندکس یکتای جزئی uq_single_platform_owner تضمین
  می‌کند دقیقاً یک مالک وجود داشته باشد — در سطح دیتابیس، نه فقط در کد.
- ثبت‌نام عمومی حذف شد (POST /auth/signup). حساب‌ها فقط با POST /admin/users
  (فقط مالک، با رمز موقت یک‌بارمصرف) یا با پذیرش لینک دعوت ساخته می‌شوند.
  در مسیر دعوت، اگر حساب وجود نداشته باشد خودِ دعوت‌شده رمز تعیین می‌کند؛ اگر
  وجود داشته باشد لینک هرگز جای reset رمز را نمی‌گیرد.
- ساخت سازمان فقط برای مالک و مدیران؛ کاربر عادی با دعوت عضو می‌شود.
- تنزل یا حذف مدیر، سازمان‌هایش را به مالک سامانه منتقل می‌کند (مالکیت + عضویت،
  با audit). حساب مالک از API تغییرناپذیر است؛ جابه‌جایی صندلی فقط با
  POST /admin/transfer-ownership و در یک تراکنش انجام می‌شود.
- افزودن POST /auth/change-password برای تبدیل رمز موقت به رمز خصوصی.
- فرانت: صفحهٔ /admin/users، صفحهٔ /account، بازنویسی /accept-invite، حذف /signup،
  گارد /onboarding برای کاربر عادی، و نشان نقش پلتفرم در پوستهٔ اپ.
- PRAGMA foreign_keys=ON برای SQLite تا ON DELETE CASCADE در dev و prod یکسان
  رفتار کند (پیش‌تر حذف کاربر، عضویت‌هایش را یتیم می‌گذاشت).
- تست: ۳۰۰ تست بک‌اند (شامل tests/test_platform_hierarchy.py با ۲۲ تست) و ۶۴ تست
  فرانت‌اند؛ ruff، mypy، tsc، eslint و build تمیز.
2026-10-13 21:58:41 +03:30

929 lines
40 KiB
Python

"""SQLAlchemy ORM tables.
This is the *only* module that knows about the relational schema. The domain
layer is expressed as plain dataclasses in ``app.domain.*``; repositories map
between the two. Keeping the schema here means a migration change never forces
a change to domain logic.
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import (
JSON,
Boolean,
DateTime,
Float,
ForeignKey,
Index,
Integer,
String,
Text,
UniqueConstraint,
text,
)
from sqlalchemy.orm import Mapped, mapped_column
from app.core.database import Base
from app.domain.base import new_id, utcnow
def _pk() -> Mapped[str]:
return mapped_column(String(32), primary_key=True, default=new_id)
def _created() -> Mapped[datetime]:
return mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
# ---------------------------------------------------------------------------
# Identity & Access
# ---------------------------------------------------------------------------
class UserRow(Base):
__tablename__ = "users"
__table_args__ = (
# Exactly one platform owner may exist — ever. A partial unique index
# makes that a *database* invariant, not just an application habit: a
# second INSERT with platform_role='owner' fails even if some future
# code path forgets to check. Both dialects support partial indexes.
Index(
"uq_single_platform_owner",
"platform_role",
unique=True,
sqlite_where=text("platform_role = 'owner'"),
postgresql_where=text("platform_role = 'owner'"),
),
Index("ix_user_platform_role", "platform_role"),
)
id: Mapped[str] = _pk()
email: Mapped[str] = mapped_column(String(320), unique=True, index=True, nullable=False)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
name: Mapped[str] = mapped_column(String(160), default="", nullable=False)
avatar_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
#: Platform-wide role: ``owner`` | ``manager`` | ``user`` (see
#: ``PlatformRole``). Exactly one row may hold ``owner``.
platform_role: Mapped[str] = mapped_column(String(16), default="user", nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = _created()
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False
)
class OrganizationRow(Base):
__tablename__ = "organizations"
id: Mapped[str] = _pk()
name: Mapped[str] = mapped_column(String(160), nullable=False)
slug: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
owner_user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
created_at: Mapped[datetime] = _created()
class MembershipRow(Base):
__tablename__ = "memberships"
__table_args__ = (
UniqueConstraint("organization_id", "user_id", name="uq_membership_org_user"),
Index("ix_membership_org_role", "organization_id", "role"),
)
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
user_id: Mapped[str] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
role: Mapped[str] = mapped_column(String(16), default="member", nullable=False)
status: Mapped[str] = mapped_column(String(16), default="active", nullable=False)
created_at: Mapped[datetime] = _created()
class InviteRow(Base):
__tablename__ = "invites"
__table_args__ = (Index("ix_invite_org_email", "organization_id", "email"),)
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
email: Mapped[str] = mapped_column(String(320), nullable=False)
role: Mapped[str] = mapped_column(String(16), default="member", nullable=False)
token_hash: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)
invited_by_user_id: Mapped[str | None] = mapped_column(
ForeignKey("users.id"), nullable=True, index=True
)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
accepted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = _created()
# ---------------------------------------------------------------------------
# AI Control Plane (platform-owner only)
# ---------------------------------------------------------------------------
class AiProviderRow(Base):
__tablename__ = "ai_providers"
id: Mapped[str] = _pk()
name: Mapped[str] = mapped_column(String(120), unique=True, nullable=False)
#: agy is the only provider type; the column stays a string so old rows
#: (mock / openai_compatible, deleted by migration c2d3e4f5b6a7) cannot
#: break the enum mapping.
type: Mapped[str] = mapped_column(String(32), default="agy", nullable=False)
base_url: Mapped[str] = mapped_column(String(512), default="", nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = _created()
class AiModelCredentialRow(Base):
__tablename__ = "ai_model_credentials"
id: Mapped[str] = _pk()
provider_id: Mapped[str] = mapped_column(
ForeignKey("ai_providers.id", ondelete="CASCADE"), nullable=False, index=True
)
name: Mapped[str] = mapped_column(String(120), nullable=False)
encrypted_api_key: Mapped[str] = mapped_column(Text, default="", nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = _created()
# --- agy account columns (empty for API-key credentials) -----------------
#: Google account email this credential belongs to; also the rotation label.
email: Mapped[str] = mapped_column(String(320), default="", nullable=False)
#: Antigravity ``cloudaicompanionProject`` id resolved during login.
project_id: Mapped[str] = mapped_column(String(160), default="", nullable=False)
#: ``active`` | ``cooldown`` | ``error`` — see ``CredentialStatus``.
status: Mapped[str] = mapped_column(String(16), default="active", nullable=False)
#: When set and in the future, the rotation pool skips this account.
cooldown_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
last_error: Mapped[str] = mapped_column(String(512), default="", nullable=False)
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
class AiModelRow(Base):
__tablename__ = "ai_models"
__table_args__ = (
UniqueConstraint("provider_id", "internal_name", name="uq_model_provider_name"),
)
id: Mapped[str] = _pk()
provider_id: Mapped[str] = mapped_column(
ForeignKey("ai_providers.id", ondelete="CASCADE"), nullable=False, index=True
)
internal_name: Mapped[str] = mapped_column(String(160), nullable=False)
display_name_for_admin: Mapped[str] = mapped_column(String(160), nullable=False)
capabilities: Mapped[list] = mapped_column(JSON, default=list, nullable=False)
input_price_per_1m: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
output_price_per_1m: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
#: agy reasoning effort ("" = provider default); see ``AiModel``.
reasoning_effort: Mapped[str] = mapped_column(String(16), default="", nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = _created()
class RoutingPolicyRow(Base):
__tablename__ = "routing_policies"
id: Mapped[str] = _pk()
name: Mapped[str] = mapped_column(String(120), unique=True, nullable=False)
description: Mapped[str] = mapped_column(Text, default="", nullable=False)
mode: Mapped[str] = mapped_column(String(16), default="balanced", nullable=False)
fallback_policy_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
budget_limits_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = _created()
class RateCardRow(Base):
__tablename__ = "rate_cards"
id: Mapped[str] = _pk()
name: Mapped[str] = mapped_column(String(120), nullable=False)
unit: Mapped[str] = mapped_column(String(40), unique=True, nullable=False)
customer_price: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
internal_cost_estimate: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = _created()
# ---------------------------------------------------------------------------
# Projects & Git
# ---------------------------------------------------------------------------
class ProjectRow(Base):
__tablename__ = "projects"
__table_args__ = (
UniqueConstraint("organization_id", "slug", name="uq_project_org_slug"),
# The project list is always scoped to one organization and ordered by
# creation, so one composite index serves both the filter and the sort.
Index("ix_project_org_created", "organization_id", "created_at"),
)
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
name: Mapped[str] = mapped_column(String(160), nullable=False)
slug: Mapped[str] = mapped_column(String(64), nullable=False)
description: Mapped[str] = mapped_column(Text, default="", nullable=False)
created_by_user_id: Mapped[str | None] = mapped_column(
ForeignKey("users.id"), nullable=True, index=True
)
default_branch: Mapped[str] = mapped_column(String(80), default="main", nullable=False)
created_at: Mapped[datetime] = _created()
class ProjectRepositoryRow(Base):
__tablename__ = "project_repositories"
id: Mapped[str] = _pk()
project_id: Mapped[str] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), unique=True, nullable=False, index=True
)
provider: Mapped[str] = mapped_column(String(32), default="gitea", nullable=False)
owner: Mapped[str] = mapped_column(String(120), default="", nullable=False)
repo_name: Mapped[str] = mapped_column(String(160), default="", nullable=False)
clone_url: Mapped[str] = mapped_column(String(512), default="", nullable=False)
web_url: Mapped[str] = mapped_column(String(512), default="", nullable=False)
default_branch: Mapped[str] = mapped_column(String(80), default="main", nullable=False)
created_at: Mapped[datetime] = _created()
class ProjectGitCredentialRow(Base):
__tablename__ = "project_git_credentials"
id: Mapped[str] = _pk()
project_id: Mapped[str] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), unique=True, nullable=False, index=True
)
username: Mapped[str] = mapped_column(String(160), nullable=False)
encrypted_password: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[datetime] = _created()
# ---------------------------------------------------------------------------
# Preview domain routing
# ---------------------------------------------------------------------------
class PreviewPortRow(Base):
__tablename__ = "preview_ports"
__table_args__ = (
UniqueConstraint("project_id", "service_name", name="uq_preview_port_project_service"),
UniqueConstraint("port", name="uq_preview_port_port"),
)
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
project_id: Mapped[str] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
)
service_name: Mapped[str] = mapped_column(String(80), default="web", nullable=False)
port: Mapped[int] = mapped_column(Integer, nullable=False)
status: Mapped[str] = mapped_column(String(16), default="active", nullable=False)
created_at: Mapped[datetime] = _created()
class PreviewDomainRow(Base):
__tablename__ = "preview_domains"
__table_args__ = (
UniqueConstraint("host", name="uq_preview_domain_host"),
Index("ix_preview_domain_org_created", "organization_id", "created_at"),
)
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
project_id: Mapped[str] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
)
host: Mapped[str] = mapped_column(String(255), nullable=False)
port: Mapped[int] = mapped_column(Integer, nullable=False)
status: Mapped[str] = mapped_column(String(16), default="active", nullable=False)
protected: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
created_by_user_id: Mapped[str | None] = mapped_column(
ForeignKey("users.id"), nullable=True, index=True
)
created_at: Mapped[datetime] = _created()
# ---------------------------------------------------------------------------
# Sandbox
# ---------------------------------------------------------------------------
class SandboxTemplateRow(Base):
__tablename__ = "sandbox_templates"
id: Mapped[str] = _pk()
name: Mapped[str] = mapped_column(String(120), unique=True, nullable=False)
provider: Mapped[str] = mapped_column(String(32), default="mock", nullable=False)
image: Mapped[str] = mapped_column(String(255), default="python:3.13-slim", nullable=False)
description: Mapped[str] = mapped_column(Text, default="", nullable=False)
default_cpu: Mapped[float] = mapped_column(Float, default=1.0, nullable=False)
default_memory_mb: Mapped[int] = mapped_column(Integer, default=1024, nullable=False)
default_disk_mb: Mapped[int] = mapped_column(Integer, default=2048, nullable=False)
is_system: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = _created()
class WorkspaceRow(Base):
__tablename__ = "workspaces"
__table_args__ = (Index("ix_workspace_project_created", "project_id", "created_at"),)
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
project_id: Mapped[str] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
)
template_id: Mapped[str] = mapped_column(
ForeignKey("sandbox_templates.id"), nullable=False, index=True
)
provider: Mapped[str] = mapped_column(String(32), default="mock", nullable=False)
external_workspace_id: Mapped[str | None] = mapped_column(String(160), nullable=True)
status: Mapped[str] = mapped_column(String(16), default="pending", nullable=False)
created_at: Mapped[datetime] = _created()
class SandboxSessionRow(Base):
__tablename__ = "sandbox_sessions"
id: Mapped[str] = _pk()
workspace_id: Mapped[str] = mapped_column(
ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, index=True
)
external_session_id: Mapped[str | None] = mapped_column(String(160), nullable=True)
status: Mapped[str] = mapped_column(String(16), default="starting", nullable=False)
started_at: Mapped[datetime] = _created()
stopped_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
last_active_at: Mapped[datetime] = _created()
class CommandRunRow(Base):
__tablename__ = "command_runs"
id: Mapped[str] = _pk()
session_id: Mapped[str] = mapped_column(
ForeignKey("sandbox_sessions.id", ondelete="CASCADE"), nullable=False, index=True
)
command: Mapped[str] = mapped_column(Text, nullable=False)
exit_code: Mapped[int | None] = mapped_column(Integer, nullable=True)
stdout_ref: Mapped[str] = mapped_column(Text, default="", nullable=False)
stderr_ref: Mapped[str] = mapped_column(Text, default="", nullable=False)
duration_ms: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
truncated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
created_at: Mapped[datetime] = _created()
class SnapshotRow(Base):
__tablename__ = "snapshots"
id: Mapped[str] = _pk()
workspace_id: Mapped[str] = mapped_column(
ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, index=True
)
name: Mapped[str] = mapped_column(String(120), nullable=False)
external_snapshot_id: Mapped[str | None] = mapped_column(String(160), nullable=True)
created_at: Mapped[datetime] = _created()
class PreviewUrlRow(Base):
__tablename__ = "preview_urls"
id: Mapped[str] = _pk()
workspace_id: Mapped[str] = mapped_column(
ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, index=True
)
port: Mapped[int] = mapped_column(Integer, nullable=False)
url: Mapped[str] = mapped_column(String(512), nullable=False)
created_at: Mapped[datetime] = _created()
class SandboxResourceLimitRow(Base):
__tablename__ = "sandbox_resource_limits"
__table_args__ = (UniqueConstraint("scope", "scope_id", name="uq_limit_scope"),)
id: Mapped[str] = _pk()
scope: Mapped[str] = mapped_column(String(32), default="organization", nullable=False)
scope_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
cpu: Mapped[float] = mapped_column(Float, default=1.0, nullable=False)
memory_mb: Mapped[int] = mapped_column(Integer, default=1024, nullable=False)
disk_mb: Mapped[int] = mapped_column(Integer, default=2048, nullable=False)
timeout_seconds: Mapped[int] = mapped_column(Integer, default=120, nullable=False)
max_output_bytes: Mapped[int] = mapped_column(Integer, default=200_000, nullable=False)
network_policy: Mapped[str] = mapped_column(String(16), default="deny_all", nullable=False)
# ---------------------------------------------------------------------------
# Workforce: templates, employees, chat, tasks, runs, artifacts
# ---------------------------------------------------------------------------
class AgentTemplateRow(Base):
__tablename__ = "agent_templates"
id: Mapped[str] = _pk()
name: Mapped[str] = mapped_column(String(120), unique=True, nullable=False)
role_key: Mapped[str] = mapped_column(String(32), nullable=False)
description: Mapped[str] = mapped_column(Text, default="", nullable=False)
default_prompt: Mapped[str] = mapped_column(Text, default="", nullable=False)
default_tools_json: Mapped[list] = mapped_column(JSON, default=list, nullable=False)
default_routing_policy_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
is_system: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = _created()
class EmployeeRow(Base):
__tablename__ = "employees"
__table_args__ = (Index("ix_employee_org_created", "organization_id", "created_at"),)
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
project_id: Mapped[str | None] = mapped_column(
ForeignKey("projects.id", ondelete="SET NULL"), nullable=True, index=True
)
template_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
name: Mapped[str] = mapped_column(String(160), nullable=False)
role_key: Mapped[str] = mapped_column(String(32), nullable=False)
status: Mapped[str] = mapped_column(String(16), default="active", nullable=False)
routing_policy_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
budget_limit_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
created_at: Mapped[datetime] = _created()
class PromptVersionRow(Base):
__tablename__ = "prompt_versions"
__table_args__ = (
UniqueConstraint("employee_id", "version", name="uq_prompt_employee_version"),
)
id: Mapped[str] = _pk()
employee_id: Mapped[str] = mapped_column(
ForeignKey("employees.id", ondelete="CASCADE"), nullable=False, index=True
)
version: Mapped[int] = mapped_column(Integer, nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
created_by_user_id: Mapped[str | None] = mapped_column(
ForeignKey("users.id"), nullable=True, index=True
)
created_at: Mapped[datetime] = _created()
class EmployeeToolPermissionRow(Base):
__tablename__ = "employee_tool_permissions"
__table_args__ = (
UniqueConstraint("employee_id", "tool_key", name="uq_employee_tool"),
)
id: Mapped[str] = _pk()
employee_id: Mapped[str] = mapped_column(
ForeignKey("employees.id", ondelete="CASCADE"), nullable=False, index=True
)
tool_key: Mapped[str] = mapped_column(String(80), nullable=False)
allowed: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
policy_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
class ThreadRow(Base):
__tablename__ = "threads"
__table_args__ = (Index("ix_thread_org_created", "organization_id", "created_at"),)
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
project_id: Mapped[str | None] = mapped_column(
ForeignKey("projects.id", ondelete="SET NULL"), nullable=True, index=True
)
title: Mapped[str] = mapped_column(String(255), default="", nullable=False)
created_by_user_id: Mapped[str | None] = mapped_column(
ForeignKey("users.id"), nullable=True, index=True
)
created_at: Mapped[datetime] = _created()
class MessageRow(Base):
__tablename__ = "messages"
# A conversation is always read as "one thread, oldest first", so the
# composite index serves the filter and the ordering in one lookup.
__table_args__ = (Index("ix_message_thread_created", "thread_id", "created_at"),)
id: Mapped[str] = _pk()
thread_id: Mapped[str] = mapped_column(
ForeignKey("threads.id", ondelete="CASCADE"), nullable=False, index=True
)
role: Mapped[str] = mapped_column(String(16), nullable=False)
content: Mapped[str] = mapped_column(Text, default="", nullable=False)
employee_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
created_at: Mapped[datetime] = _created()
class TaskRow(Base):
__tablename__ = "tasks"
__table_args__ = (
Index("ix_task_org_status", "organization_id", "status"),
Index("ix_task_org_created", "organization_id", "created_at"),
)
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
project_id: Mapped[str | None] = mapped_column(
ForeignKey("projects.id", ondelete="SET NULL"), nullable=True, index=True
)
thread_id: Mapped[str | None] = mapped_column(
ForeignKey("threads.id", ondelete="SET NULL"), nullable=True, index=True
)
title: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str] = mapped_column(Text, default="", nullable=False)
status: Mapped[str] = mapped_column(String(24), default="new", nullable=False)
assigned_employee_id: Mapped[str | None] = mapped_column(
String(32), nullable=True, index=True
)
created_by_user_id: Mapped[str | None] = mapped_column(
ForeignKey("users.id"), nullable=True, index=True
)
created_at: Mapped[datetime] = _created()
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False
)
class RunRow(Base):
__tablename__ = "runs"
# A run has no organization_id of its own: it is scoped through its task,
# which is the object that carries the tenant. Indexing a column that does
# not exist here would be wrong, so the composite stays on task_id.
__table_args__ = (Index("ix_run_task_created", "task_id", "created_at"),)
id: Mapped[str] = _pk()
task_id: Mapped[str] = mapped_column(
ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False, index=True
)
employee_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
status: Mapped[str] = mapped_column(String(24), default="queued", nullable=False)
error: Mapped[str | None] = mapped_column(Text, nullable=True)
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = _created()
class RunStepRow(Base):
__tablename__ = "run_steps"
__table_args__ = (Index("ix_run_step_run_index", "run_id", "index"),)
id: Mapped[str] = _pk()
run_id: Mapped[str] = mapped_column(
ForeignKey("runs.id", ondelete="CASCADE"), nullable=False, index=True
)
index: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
kind: Mapped[str] = mapped_column(String(40), default="note", nullable=False)
title: Mapped[str] = mapped_column(String(255), default="", nullable=False)
payload_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
created_at: Mapped[datetime] = _created()
class ToolCallRow(Base):
__tablename__ = "tool_calls"
__table_args__ = (
Index("ix_tool_call_run_created", "run_id", "created_at"),
# The pending-approval queue is "one organization, still awaiting a
# decision", which is a filter on status plus a join back to the run.
Index("ix_tool_call_status", "status"),
)
id: Mapped[str] = _pk()
run_id: Mapped[str] = mapped_column(
ForeignKey("runs.id", ondelete="CASCADE"), nullable=False, index=True
)
tool_key: Mapped[str] = mapped_column(String(80), nullable=False)
arguments_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
result_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
status: Mapped[str] = mapped_column(String(24), default="pending", nullable=False)
requires_approval: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
approved_by_user_id: Mapped[str | None] = mapped_column(
ForeignKey("users.id"), nullable=True, index=True
)
created_at: Mapped[datetime] = _created()
class ArtifactRow(Base):
__tablename__ = "artifacts"
__table_args__ = (Index("ix_artifact_org_created", "organization_id", "created_at"),)
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
project_id: Mapped[str | None] = mapped_column(
ForeignKey("projects.id", ondelete="SET NULL"), nullable=True, index=True
)
run_id: Mapped[str | None] = mapped_column(
ForeignKey("runs.id", ondelete="SET NULL"), nullable=True, index=True
)
name: Mapped[str] = mapped_column(String(255), nullable=False)
kind: Mapped[str] = mapped_column(String(32), default="file", nullable=False)
content_ref: Mapped[str] = mapped_column(Text, default="", nullable=False)
size_bytes: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
created_at: Mapped[datetime] = _created()
# ---------------------------------------------------------------------------
# Billing & Usage
# ---------------------------------------------------------------------------
class PlanRow(Base):
__tablename__ = "plans"
id: Mapped[str] = _pk()
name: Mapped[str] = mapped_column(String(120), unique=True, nullable=False)
monthly_price: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
included_credit: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
features_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = _created()
class SubscriptionRow(Base):
__tablename__ = "subscriptions"
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
plan_id: Mapped[str] = mapped_column(ForeignKey("plans.id"), nullable=False, index=True)
status: Mapped[str] = mapped_column(String(16), default="active", nullable=False)
external_subscription_id: Mapped[str | None] = mapped_column(String(160), nullable=True)
current_period_end: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
created_at: Mapped[datetime] = _created()
class CreditWalletRow(Base):
__tablename__ = "credit_wallets"
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), unique=True, nullable=False, index=True
)
balance: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
currency: Mapped[str] = mapped_column(String(8), default="USD", nullable=False)
low_balance_threshold: Mapped[float] = mapped_column(Float, default=5.0, nullable=False)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False
)
class UsageEventRow(Base):
__tablename__ = "usage_events"
__table_args__ = (
Index("ix_usage_org_created", "organization_id", "created_at"),
Index("ix_usage_org_unit", "organization_id", "unit"),
)
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
project_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
user_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
employee_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
run_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
unit: Mapped[str] = mapped_column(String(40), nullable=False)
quantity: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
unit_price: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
internal_cost_estimate: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
total_price: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
metadata_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
created_at: Mapped[datetime] = _created()
class InvoiceRow(Base):
__tablename__ = "invoices"
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
amount: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
currency: Mapped[str] = mapped_column(String(8), default="USD", nullable=False)
status: Mapped[str] = mapped_column(String(16), default="draft", nullable=False)
external_invoice_id: Mapped[str | None] = mapped_column(String(160), nullable=True)
period_start: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
period_end: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = _created()
class PaymentRow(Base):
__tablename__ = "payments"
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
amount: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
status: Mapped[str] = mapped_column(String(16), default="pending", nullable=False)
provider: Mapped[str] = mapped_column(String(32), default="mock", nullable=False)
external_payment_id: Mapped[str | None] = mapped_column(String(160), nullable=True)
invoice_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
created_at: Mapped[datetime] = _created()
# ---------------------------------------------------------------------------
# Knowledge
# ---------------------------------------------------------------------------
class KnowledgeSourceRow(Base):
__tablename__ = "knowledge_sources"
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
project_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
kind: Mapped[str] = mapped_column(String(16), default="text", nullable=False)
uri: Mapped[str] = mapped_column(String(512), default="", nullable=False)
created_at: Mapped[datetime] = _created()
class DocumentRow(Base):
__tablename__ = "documents"
id: Mapped[str] = _pk()
source_id: Mapped[str] = mapped_column(
ForeignKey("knowledge_sources.id", ondelete="CASCADE"), nullable=False, index=True
)
title: Mapped[str] = mapped_column(String(255), default="", nullable=False)
content: Mapped[str] = mapped_column(Text, default="", nullable=False)
created_at: Mapped[datetime] = _created()
class DocumentChunkRow(Base):
__tablename__ = "document_chunks"
__table_args__ = (Index("ix_chunk_document_index", "document_id", "index"),)
id: Mapped[str] = _pk()
document_id: Mapped[str] = mapped_column(
ForeignKey("documents.id", ondelete="CASCADE"), nullable=False, index=True
)
index: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
content: Mapped[str] = mapped_column(Text, default="", nullable=False)
embedding_json: Mapped[list] = mapped_column(JSON, default=list, nullable=False)
class EmbeddingJobRow(Base):
__tablename__ = "embedding_jobs"
id: Mapped[str] = _pk()
source_id: Mapped[str] = mapped_column(
ForeignKey("knowledge_sources.id", ondelete="CASCADE"), nullable=False, index=True
)
status: Mapped[str] = mapped_column(String(16), default="pending", nullable=False)
error: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = _created()
class RetrievalPolicyRow(Base):
__tablename__ = "retrieval_policies"
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
name: Mapped[str] = mapped_column(String(120), nullable=False)
scope_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
created_at: Mapped[datetime] = _created()
# ---------------------------------------------------------------------------
# Audit & Observability
# ---------------------------------------------------------------------------
class AuditEventRow(Base):
__tablename__ = "audit_events"
__table_args__ = (
Index("ix_audit_org_created", "organization_id", "created_at"),
Index("ix_audit_action", "action"),
)
id: Mapped[str] = _pk()
organization_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
actor_user_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
action: Mapped[str] = mapped_column(String(80), nullable=False)
target_type: Mapped[str] = mapped_column(String(40), default="", nullable=False)
target_id: Mapped[str] = mapped_column(String(64), default="", nullable=False)
metadata_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
ip_address: Mapped[str] = mapped_column(String(64), default="", nullable=False)
user_agent: Mapped[str] = mapped_column(String(255), default="", nullable=False)
created_at: Mapped[datetime] = _created()
class TraceRow(Base):
__tablename__ = "traces"
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
run_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
name: Mapped[str] = mapped_column(String(160), default="", nullable=False)
created_at: Mapped[datetime] = _created()
class SpanRow(Base):
__tablename__ = "spans"
id: Mapped[str] = _pk()
trace_id: Mapped[str] = mapped_column(
ForeignKey("traces.id", ondelete="CASCADE"), nullable=False, index=True
)
parent_span_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
name: Mapped[str] = mapped_column(String(160), nullable=False)
kind: Mapped[str] = mapped_column(String(32), default="internal", nullable=False)
attributes_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
started_at: Mapped[datetime] = _created()
ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
class ErrorEventRow(Base):
"""Persisted failures — the table the error console reads.
Wide on purpose: a row that lacks the request path or the exception class
forces whoever is debugging to reproduce the bug to learn what it was. The
indexes cover the three access patterns — filter by tenant, filter by
"still open", and group by fingerprint.
"""
__tablename__ = "error_events"
__table_args__ = (
# Grouping view: "how often did this exact bug happen, and when last?"
Index("ix_error_events_fingerprint_created", "fingerprint", "created_at"),
# Feed view: "show me the newest failures for this tenant."
Index("ix_error_events_org_created", "organization_id", "created_at"),
)
id: Mapped[str] = _pk()
organization_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
run_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
kind: Mapped[str] = mapped_column(String(80), nullable=False)
message: Mapped[str] = mapped_column(Text, default="", nullable=False)
stack: Mapped[str] = mapped_column(Text, default="", nullable=False)
metadata_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
# --- classification -----------------------------------------------------
source: Mapped[str] = mapped_column(String(16), default="http", nullable=False)
severity: Mapped[str] = mapped_column(String(16), default="error", nullable=False)
error_type: Mapped[str] = mapped_column(String(200), default="", nullable=False)
#: Groups identical failures. Empty string rather than NULL so the grouping
#: index never has to deal with a NULL key.
fingerprint: Mapped[str] = mapped_column(String(64), default="", nullable=False, index=True)
# --- transport context --------------------------------------------------
status_code: Mapped[int | None] = mapped_column(Integer, nullable=True)
method: Mapped[str] = mapped_column(String(10), default="", nullable=False)
path: Mapped[str] = mapped_column(String(512), default="", nullable=False)
# --- actor context ------------------------------------------------------
user_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
request_id: Mapped[str] = mapped_column(String(64), default="", nullable=False)
ip: Mapped[str] = mapped_column(String(64), default="", nullable=False)
user_agent: Mapped[str] = mapped_column(String(255), default="", nullable=False)
# --- triage -------------------------------------------------------------
resolved_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True, index=True
)
resolved_note: Mapped[str] = mapped_column(Text, default="", nullable=False)
created_at: Mapped[datetime] = _created()
class QualityMetricRow(Base):
__tablename__ = "quality_metrics"
__table_args__ = (Index("ix_metric_org_name", "organization_id", "name"),)
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
name: Mapped[str] = mapped_column(String(120), nullable=False)
value: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
dimensions_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
created_at: Mapped[datetime] = _created()