xml-pipeline/bloxserver/api/models/tables.py
dullfig a5c00c1e90 Add BloxServer API scaffold + architecture docs
BloxServer API (FastAPI + SQLAlchemy async):
- Database models: users, flows, triggers, executions, usage tracking
- Clerk JWT auth with dev mode bypass for local testing
- SQLite support for local dev, PostgreSQL for production
- CRUD routes for flows, triggers, executions
- Public webhook endpoint with token auth
- Health/readiness endpoints
- Pydantic schemas with camelCase aliases for frontend
- Docker + docker-compose setup

Architecture documentation:
- Librarian architecture with RLM-powered query engine
- Stripe billing integration (usage-based, trials, webhooks)
- LLM abstraction layer (rate limiting, semantic cache, failover)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 22:04:25 -08:00

381 lines
12 KiB
Python

"""
SQLAlchemy ORM models for BloxServer.
These map to the Pydantic models in schemas.py and TypeScript types in types.ts.
"""
from __future__ import annotations
import enum
from datetime import datetime
from typing import Any
from uuid import uuid4
from sqlalchemy import (
JSON,
Boolean,
DateTime,
Enum,
ForeignKey,
Index,
Integer,
LargeBinary,
Numeric,
String,
Text,
func,
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from bloxserver.api.models.database import Base
# =============================================================================
# Enums
# =============================================================================
class Tier(str, enum.Enum):
"""User subscription tier."""
FREE = "free"
PRO = "pro"
ENTERPRISE = "enterprise"
HIGH_FREQUENCY = "high_frequency"
class BillingStatus(str, enum.Enum):
"""Subscription billing status."""
ACTIVE = "active"
TRIALING = "trialing"
PAST_DUE = "past_due"
CANCELED = "canceled"
CANCELING = "canceling"
class FlowStatus(str, enum.Enum):
"""Flow runtime status."""
STOPPED = "stopped"
STARTING = "starting"
RUNNING = "running"
STOPPING = "stopping"
ERROR = "error"
class TriggerType(str, enum.Enum):
"""How a flow can be triggered."""
WEBHOOK = "webhook"
SCHEDULE = "schedule"
MANUAL = "manual"
class ExecutionStatus(str, enum.Enum):
"""Status of a flow execution."""
RUNNING = "running"
SUCCESS = "success"
ERROR = "error"
TIMEOUT = "timeout"
# =============================================================================
# Users (synced from Clerk)
# =============================================================================
class UserRecord(Base):
"""User account, synced from Clerk."""
__tablename__ = "users"
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
clerk_id: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
email: Mapped[str] = mapped_column(String(255), nullable=False)
name: Mapped[str | None] = mapped_column(String(255))
avatar_url: Mapped[str | None] = mapped_column(Text)
# Stripe integration
stripe_customer_id: Mapped[str | None] = mapped_column(String(255), unique=True)
stripe_subscription_id: Mapped[str | None] = mapped_column(String(255))
stripe_subscription_item_id: Mapped[str | None] = mapped_column(String(255))
# Billing state (cached from Stripe)
tier: Mapped[Tier] = mapped_column(Enum(Tier), default=Tier.FREE)
billing_status: Mapped[BillingStatus] = mapped_column(
Enum(BillingStatus), default=BillingStatus.ACTIVE
)
trial_ends_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
current_period_start: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
current_period_end: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
# Relationships
flows: Mapped[list[FlowRecord]] = relationship(back_populates="user", cascade="all, delete-orphan")
api_keys: Mapped[list[UserApiKeyRecord]] = relationship(back_populates="user", cascade="all, delete-orphan")
usage_records: Mapped[list[UsageRecord]] = relationship(back_populates="user", cascade="all, delete-orphan")
__table_args__ = (
Index("idx_users_clerk_id", "clerk_id"),
Index("idx_users_stripe_customer", "stripe_customer_id"),
)
# =============================================================================
# Flows
# =============================================================================
class FlowRecord(Base):
"""A user's workflow/flow."""
__tablename__ = "flows"
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
user_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
description: Mapped[str | None] = mapped_column(String(500))
# The actual workflow definition
organism_yaml: Mapped[str] = mapped_column(Text, nullable=False, default="")
# React Flow canvas state (JSON)
canvas_state: Mapped[dict[str, Any] | None] = mapped_column(JSON)
# Runtime state
status: Mapped[FlowStatus] = mapped_column(Enum(FlowStatus), default=FlowStatus.STOPPED)
container_id: Mapped[str | None] = mapped_column(String(255))
error_message: Mapped[str | None] = mapped_column(Text)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
# Relationships
user: Mapped[UserRecord] = relationship(back_populates="flows")
triggers: Mapped[list[TriggerRecord]] = relationship(back_populates="flow", cascade="all, delete-orphan")
executions: Mapped[list[ExecutionRecord]] = relationship(back_populates="flow", cascade="all, delete-orphan")
__table_args__ = (
Index("idx_flows_user_id", "user_id"),
Index("idx_flows_status", "status"),
)
# =============================================================================
# Triggers
# =============================================================================
class TriggerRecord(Base):
"""A trigger that can start a flow."""
__tablename__ = "triggers"
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
flow_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("flows.id", ondelete="CASCADE"), nullable=False
)
type: Mapped[TriggerType] = mapped_column(Enum(TriggerType), nullable=False)
name: Mapped[str] = mapped_column(String(100), nullable=False)
# Trigger configuration (JSON)
config: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default=dict)
# Webhook-specific fields
webhook_token: Mapped[str | None] = mapped_column(String(64), unique=True)
webhook_url: Mapped[str | None] = mapped_column(Text)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
# Relationships
flow: Mapped[FlowRecord] = relationship(back_populates="triggers")
executions: Mapped[list[ExecutionRecord]] = relationship(back_populates="trigger")
__table_args__ = (
Index("idx_triggers_flow_id", "flow_id"),
Index("idx_triggers_webhook_token", "webhook_token"),
)
# =============================================================================
# Executions
# =============================================================================
class ExecutionRecord(Base):
"""A single execution/run of a flow."""
__tablename__ = "executions"
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
flow_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("flows.id", ondelete="CASCADE"), nullable=False
)
trigger_id: Mapped[UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("triggers.id", ondelete="SET NULL")
)
trigger_type: Mapped[TriggerType] = mapped_column(Enum(TriggerType), nullable=False)
# Execution state
status: Mapped[ExecutionStatus] = mapped_column(
Enum(ExecutionStatus), default=ExecutionStatus.RUNNING
)
error_message: Mapped[str | None] = mapped_column(Text)
# Payloads (JSON strings for flexibility)
input_payload: Mapped[str | None] = mapped_column(Text)
output_payload: Mapped[str | None] = mapped_column(Text)
# Timing
started_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
duration_ms: Mapped[int | None] = mapped_column(Integer)
# Relationships
flow: Mapped[FlowRecord] = relationship(back_populates="executions")
trigger: Mapped[TriggerRecord | None] = relationship(back_populates="executions")
__table_args__ = (
Index("idx_executions_flow_id", "flow_id"),
Index("idx_executions_started_at", "started_at"),
Index("idx_executions_status", "status"),
)
# =============================================================================
# User API Keys (BYOK)
# =============================================================================
class UserApiKeyRecord(Base):
"""User's own API keys for BYOK (Bring Your Own Key)."""
__tablename__ = "user_api_keys"
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
user_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
provider: Mapped[str] = mapped_column(String(50), nullable=False)
# Encrypted API key
encrypted_key: Mapped[bytes] = mapped_column(LargeBinary, nullable=False)
key_hint: Mapped[str | None] = mapped_column(String(20)) # Last few chars for display
# Validation state
is_valid: Mapped[bool] = mapped_column(Boolean, default=True)
last_error: Mapped[str | None] = mapped_column(String(255))
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
# Relationships
user: Mapped[UserRecord] = relationship(back_populates="api_keys")
__table_args__ = (
Index("idx_user_api_keys_user_provider", "user_id", "provider", unique=True),
)
# =============================================================================
# Usage Tracking
# =============================================================================
class UsageRecord(Base):
"""Usage tracking for billing."""
__tablename__ = "usage_records"
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
user_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
period_start: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False
)
# Metrics
workflow_runs: Mapped[int] = mapped_column(Integer, default=0)
llm_tokens_in: Mapped[int] = mapped_column(Integer, default=0)
llm_tokens_out: Mapped[int] = mapped_column(Integer, default=0)
wasm_cpu_seconds: Mapped[float] = mapped_column(Numeric(10, 2), default=0)
storage_gb_hours: Mapped[float] = mapped_column(Numeric(10, 2), default=0)
# Stripe sync state
last_synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
last_synced_runs: Mapped[int] = mapped_column(Integer, default=0)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
# Relationships
user: Mapped[UserRecord] = relationship(back_populates="usage_records")
__table_args__ = (
Index("idx_usage_user_period", "user_id", "period_start", unique=True),
)
# =============================================================================
# Stripe Events (Idempotency)
# =============================================================================
class StripeEventRecord(Base):
"""Processed Stripe webhook events for idempotency."""
__tablename__ = "stripe_events"
event_id: Mapped[str] = mapped_column(String(255), primary_key=True)
event_type: Mapped[str] = mapped_column(String(100), nullable=False)
processed_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
payload: Mapped[dict[str, Any] | None] = mapped_column(JSON)
__table_args__ = (
Index("idx_stripe_events_processed", "processed_at"),
)