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>
381 lines
12 KiB
Python
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"),
|
|
)
|