From 03516bb9f7ba06517036df70f6510995b05d8745 Mon Sep 17 00:00:00 2001 From: dullfig Date: Tue, 20 Jan 2026 22:16:20 -0800 Subject: [PATCH] Add Nextra API contract (TypeScript + Pydantic) Defines shared API contract between frontend and backend: - types.ts: TypeScript interfaces for Next.js frontend - models.py: Matching Pydantic models for FastAPI backend Covers: User, Flow, Trigger, Execution, WasmModule, Marketplace, ProjectMemory, and pagination/error types. Co-Authored-By: Claude Opus 4.5 --- docs/nextra-api-contract/models.py | 475 +++++++++++++++++++++++++++++ docs/nextra-api-contract/types.ts | 340 +++++++++++++++++++++ 2 files changed, 815 insertions(+) create mode 100644 docs/nextra-api-contract/models.py create mode 100644 docs/nextra-api-contract/types.ts diff --git a/docs/nextra-api-contract/models.py b/docs/nextra-api-contract/models.py new file mode 100644 index 0000000..7602539 --- /dev/null +++ b/docs/nextra-api-contract/models.py @@ -0,0 +1,475 @@ +""" +Nextra API Contract - Pydantic Models + +These models define the API contract between frontend and backend. +They match the TypeScript types in types.ts. + +Usage: + from nextra.api.models import Flow, CreateFlowRequest +""" + +from __future__ import annotations + +from datetime import datetime +from enum import Enum +from typing import Any, Generic, Literal, TypeVar +from uuid import UUID + +from pydantic import BaseModel, Field + + +# ============================================================================= +# Common Types +# ============================================================================= + +T = TypeVar("T") + + +class PaginatedResponse(BaseModel, Generic[T]): + """Paginated list response.""" + + items: list[T] + total: int + page: int + page_size: int = Field(alias="pageSize") + has_more: bool = Field(alias="hasMore") + + class Config: + populate_by_name = True + + +class ApiError(BaseModel): + """API error response.""" + + code: str + message: str + details: dict[str, Any] | None = None + + +# ============================================================================= +# User (synced from Clerk) +# ============================================================================= + +class Tier(str, Enum): + FREE = "free" + PAID = "paid" + PRO = "pro" + ENTERPRISE = "enterprise" + + +class User(BaseModel): + """User account (synced from Clerk).""" + + id: UUID + clerk_id: str = Field(alias="clerkId") + email: str + name: str | None = None + avatar_url: str | None = Field(default=None, alias="avatarUrl") + tier: Tier = Tier.FREE + created_at: datetime = Field(alias="createdAt") + + class Config: + populate_by_name = True + + +# ============================================================================= +# Flows +# ============================================================================= + +class FlowStatus(str, Enum): + STOPPED = "stopped" + STARTING = "starting" + RUNNING = "running" + STOPPING = "stopping" + ERROR = "error" + + +class CanvasNode(BaseModel): + """A node in the React Flow canvas.""" + + id: str + type: str + position: dict[str, float] + data: dict[str, Any] + + +class CanvasEdge(BaseModel): + """An edge connecting nodes in the canvas.""" + + id: str + source: str + target: str + source_handle: str | None = Field(default=None, alias="sourceHandle") + target_handle: str | None = Field(default=None, alias="targetHandle") + + class Config: + populate_by_name = True + + +class CanvasState(BaseModel): + """React Flow canvas state.""" + + nodes: list[CanvasNode] + edges: list[CanvasEdge] + viewport: dict[str, float] + + +class Flow(BaseModel): + """A user's workflow/flow.""" + + id: UUID + user_id: UUID = Field(alias="userId") + name: str + description: str | None = None + organism_yaml: str = Field(alias="organismYaml") + canvas_state: CanvasState | None = Field(default=None, alias="canvasState") + status: FlowStatus = FlowStatus.STOPPED + container_id: str | None = Field(default=None, alias="containerId") + error_message: str | None = Field(default=None, alias="errorMessage") + created_at: datetime = Field(alias="createdAt") + updated_at: datetime = Field(alias="updatedAt") + + class Config: + populate_by_name = True + + +class FlowSummary(BaseModel): + """Abbreviated flow for list views.""" + + id: UUID + name: str + description: str | None = None + status: FlowStatus + updated_at: datetime = Field(alias="updatedAt") + + class Config: + populate_by_name = True + + +class CreateFlowRequest(BaseModel): + """Request to create a new flow.""" + + name: str = Field(min_length=1, max_length=100) + description: str | None = Field(default=None, max_length=500) + organism_yaml: str | None = Field(default=None, alias="organismYaml") + + class Config: + populate_by_name = True + + +class UpdateFlowRequest(BaseModel): + """Request to update a flow.""" + + name: str | None = Field(default=None, min_length=1, max_length=100) + description: str | None = Field(default=None, max_length=500) + organism_yaml: str | None = Field(default=None, alias="organismYaml") + canvas_state: CanvasState | None = Field(default=None, alias="canvasState") + + class Config: + populate_by_name = True + + +# ============================================================================= +# Triggers +# ============================================================================= + +class TriggerType(str, Enum): + WEBHOOK = "webhook" + SCHEDULE = "schedule" + MANUAL = "manual" + + +class WebhookTriggerConfig(BaseModel): + """Config for webhook triggers.""" + + type: Literal["webhook"] = "webhook" + + +class ScheduleTriggerConfig(BaseModel): + """Config for scheduled triggers.""" + + type: Literal["schedule"] = "schedule" + cron: str = Field(description="Cron expression") + timezone: str = "UTC" + + +class ManualTriggerConfig(BaseModel): + """Config for manual triggers.""" + + type: Literal["manual"] = "manual" + + +TriggerConfig = WebhookTriggerConfig | ScheduleTriggerConfig | ManualTriggerConfig + + +class Trigger(BaseModel): + """A trigger that can start a flow.""" + + id: UUID + flow_id: UUID = Field(alias="flowId") + type: TriggerType + name: str + config: TriggerConfig + webhook_token: str | None = Field(default=None, alias="webhookToken") + webhook_url: str | None = Field(default=None, alias="webhookUrl") + created_at: datetime = Field(alias="createdAt") + + class Config: + populate_by_name = True + + +class CreateTriggerRequest(BaseModel): + """Request to create a trigger.""" + + type: TriggerType + name: str = Field(min_length=1, max_length=100) + config: TriggerConfig + + +# ============================================================================= +# Executions +# ============================================================================= + +class ExecutionStatus(str, Enum): + RUNNING = "running" + SUCCESS = "success" + ERROR = "error" + TIMEOUT = "timeout" + + +class Execution(BaseModel): + """A single execution/run of a flow.""" + + id: UUID + flow_id: UUID = Field(alias="flowId") + trigger_id: UUID | None = Field(default=None, alias="triggerId") + trigger_type: TriggerType = Field(alias="triggerType") + status: ExecutionStatus + started_at: datetime = Field(alias="startedAt") + completed_at: datetime | None = Field(default=None, alias="completedAt") + duration_ms: int | None = Field(default=None, alias="durationMs") + error_message: str | None = Field(default=None, alias="errorMessage") + input_payload: str | None = Field(default=None, alias="inputPayload") + output_payload: str | None = Field(default=None, alias="outputPayload") + + class Config: + populate_by_name = True + + +class ExecutionSummary(BaseModel): + """Abbreviated execution for list views.""" + + id: UUID + status: ExecutionStatus + trigger_type: TriggerType = Field(alias="triggerType") + started_at: datetime = Field(alias="startedAt") + duration_ms: int | None = Field(default=None, alias="durationMs") + + class Config: + populate_by_name = True + + +# ============================================================================= +# WASM Modules (Pro+) +# ============================================================================= + +class WasmModule(BaseModel): + """A custom WASM module.""" + + id: UUID + user_id: UUID = Field(alias="userId") + name: str + description: str | None = None + wit_interface: str | None = Field(default=None, alias="witInterface") + size_bytes: int = Field(alias="sizeBytes") + created_at: datetime = Field(alias="createdAt") + updated_at: datetime = Field(alias="updatedAt") + + class Config: + populate_by_name = True + + +class WasmModuleSummary(BaseModel): + """Abbreviated module for list views.""" + + id: UUID + name: str + description: str | None = None + created_at: datetime = Field(alias="createdAt") + + class Config: + populate_by_name = True + + +class CreateWasmModuleRequest(BaseModel): + """Request to create a WASM module (file uploaded separately).""" + + name: str = Field(min_length=1, max_length=100) + description: str | None = Field(default=None, max_length=500) + wit_interface: str | None = Field(default=None, alias="witInterface") + + class Config: + populate_by_name = True + + +# ============================================================================= +# Marketplace +# ============================================================================= + +class MarketplaceListingType(str, Enum): + TOOL = "tool" + FLOW = "flow" + + +class ToolContent(BaseModel): + """Content for a tool listing.""" + + type: Literal["tool"] = "tool" + wasm_module_id: UUID = Field(alias="wasmModuleId") + wit_interface: str = Field(alias="witInterface") + example_usage: str = Field(alias="exampleUsage") + + class Config: + populate_by_name = True + + +class FlowTemplateContent(BaseModel): + """Content for a flow template listing.""" + + type: Literal["flow"] = "flow" + organism_yaml: str = Field(alias="organismYaml") + canvas_state: CanvasState = Field(alias="canvasState") + + class Config: + populate_by_name = True + + +MarketplaceContent = ToolContent | FlowTemplateContent + + +class MarketplaceListing(BaseModel): + """A marketplace listing (tool or flow template).""" + + id: UUID + author_id: UUID = Field(alias="authorId") + author_name: str = Field(alias="authorName") + type: MarketplaceListingType + name: str + description: str + category: str + tags: list[str] + downloads: int = 0 + rating: float | None = None + content: MarketplaceContent + created_at: datetime = Field(alias="createdAt") + updated_at: datetime = Field(alias="updatedAt") + + class Config: + populate_by_name = True + + +class MarketplaceListingSummary(BaseModel): + """Abbreviated listing for browse views.""" + + id: UUID + author_name: str = Field(alias="authorName") + type: MarketplaceListingType + name: str + description: str + category: str + downloads: int + rating: float | None = None + + class Config: + populate_by_name = True + + +class PublishToMarketplaceRequest(BaseModel): + """Request to publish to marketplace.""" + + type: MarketplaceListingType + name: str = Field(min_length=1, max_length=100) + description: str = Field(min_length=10, max_length=2000) + category: str + tags: list[str] = Field(default_factory=list, max_length=10) + content: MarketplaceContent + + +# ============================================================================= +# Project Memory (Pro+) +# ============================================================================= + +class ProjectMemory(BaseModel): + """Project memory status for a flow.""" + + flow_id: UUID = Field(alias="flowId") + enabled: bool = False + used_bytes: int = Field(default=0, alias="usedBytes") + max_bytes: int = Field(alias="maxBytes") + keys: list[str] = Field(default_factory=list) + + class Config: + populate_by_name = True + + +class MemoryEntry(BaseModel): + """A single memory entry.""" + + key: str + value: str # JSON string + size_bytes: int = Field(alias="sizeBytes") + updated_at: datetime = Field(alias="updatedAt") + + class Config: + populate_by_name = True + + +# ============================================================================= +# Logs +# ============================================================================= + +class LogLevel(str, Enum): + DEBUG = "debug" + INFO = "info" + WARNING = "warning" + ERROR = "error" + + +class LogEntry(BaseModel): + """A log entry from a running flow.""" + + timestamp: datetime + level: LogLevel + message: str + metadata: dict[str, Any] | None = None + + +# ============================================================================= +# Stats +# ============================================================================= + +class FlowStats(BaseModel): + """Statistics for a single flow.""" + + flow_id: UUID = Field(alias="flowId") + executions_total: int = Field(alias="executionsTotal") + executions_success: int = Field(alias="executionsSuccess") + executions_error: int = Field(alias="executionsError") + avg_duration_ms: float = Field(alias="avgDurationMs") + last_executed_at: datetime | None = Field(default=None, alias="lastExecutedAt") + + class Config: + populate_by_name = True + + +class UsageStats(BaseModel): + """Usage statistics for billing/limits.""" + + user_id: UUID = Field(alias="userId") + period: Literal["day", "month"] + flow_count: int = Field(alias="flowCount") + execution_count: int = Field(alias="executionCount") + execution_limit: int = Field(alias="executionLimit") + + class Config: + populate_by_name = True diff --git a/docs/nextra-api-contract/types.ts b/docs/nextra-api-contract/types.ts new file mode 100644 index 0000000..697c06a --- /dev/null +++ b/docs/nextra-api-contract/types.ts @@ -0,0 +1,340 @@ +/** + * Nextra API Contract - TypeScript Types + * + * These types define the API contract between frontend and backend. + * The FastAPI backend uses matching Pydantic models. + * + * Usage in frontend: + * import type { Flow, CreateFlowRequest } from '@/types/api'; + */ + +// ============================================================================= +// Common Types +// ============================================================================= + +export type UUID = string; +export type ISODateTime = string; + +// ============================================================================= +// User (synced from Clerk) +// ============================================================================= + +export interface User { + id: UUID; + clerkId: string; + email: string; + name: string | null; + avatarUrl: string | null; + tier: 'free' | 'paid' | 'pro' | 'enterprise'; + createdAt: ISODateTime; +} + +// ============================================================================= +// Flows +// ============================================================================= + +export type FlowStatus = 'stopped' | 'starting' | 'running' | 'stopping' | 'error'; + +export interface Flow { + id: UUID; + userId: UUID; + name: string; + description: string | null; + organismYaml: string; + canvasState: CanvasState | null; + status: FlowStatus; + containerId: string | null; + errorMessage: string | null; + createdAt: ISODateTime; + updatedAt: ISODateTime; +} + +export interface FlowSummary { + id: UUID; + name: string; + description: string | null; + status: FlowStatus; + updatedAt: ISODateTime; +} + +export interface CreateFlowRequest { + name: string; + description?: string; + organismYaml?: string; // Default template if not provided +} + +export interface UpdateFlowRequest { + name?: string; + description?: string; + organismYaml?: string; + canvasState?: CanvasState; +} + +// ============================================================================= +// Canvas State (React Flow) +// ============================================================================= + +export interface CanvasState { + nodes: CanvasNode[]; + edges: CanvasEdge[]; + viewport: { x: number; y: number; zoom: number }; +} + +export interface CanvasNode { + id: string; + type: NodeType; + position: { x: number; y: number }; + data: NodeData; +} + +export type NodeType = + | 'trigger' + | 'llmCall' + | 'httpRequest' + | 'codeBlock' + | 'conditional' + | 'output' + | 'custom'; + +export interface NodeData { + name: string; + label: string; + description?: string; + handler?: string; + payloadClass?: string; + isAgent?: boolean; + config?: Record; +} + +export interface CanvasEdge { + id: string; + source: string; + target: string; + sourceHandle?: string; + targetHandle?: string; +} + +// ============================================================================= +// Triggers +// ============================================================================= + +export type TriggerType = 'webhook' | 'schedule' | 'manual'; + +export interface Trigger { + id: UUID; + flowId: UUID; + type: TriggerType; + name: string; + config: TriggerConfig; + webhookToken?: string; // Only for webhook triggers + webhookUrl?: string; // Full URL for webhook triggers + createdAt: ISODateTime; +} + +export type TriggerConfig = + | WebhookTriggerConfig + | ScheduleTriggerConfig + | ManualTriggerConfig; + +export interface WebhookTriggerConfig { + type: 'webhook'; +} + +export interface ScheduleTriggerConfig { + type: 'schedule'; + cron: string; // Cron expression + timezone?: string; // Default: UTC +} + +export interface ManualTriggerConfig { + type: 'manual'; +} + +export interface CreateTriggerRequest { + type: TriggerType; + name: string; + config: TriggerConfig; +} + +// ============================================================================= +// Executions (Run History) +// ============================================================================= + +export type ExecutionStatus = 'running' | 'success' | 'error' | 'timeout'; + +export interface Execution { + id: UUID; + flowId: UUID; + triggerId: UUID | null; + triggerType: TriggerType; + status: ExecutionStatus; + startedAt: ISODateTime; + completedAt: ISODateTime | null; + durationMs: number | null; + errorMessage: string | null; + inputPayload: string | null; // JSON string + outputPayload: string | null; // JSON string +} + +export interface ExecutionSummary { + id: UUID; + status: ExecutionStatus; + triggerType: TriggerType; + startedAt: ISODateTime; + durationMs: number | null; +} + +// ============================================================================= +// WASM Modules (Pro+) +// ============================================================================= + +export interface WasmModule { + id: UUID; + userId: UUID; + name: string; + description: string | null; + witInterface: string | null; + sizeBytes: number; + createdAt: ISODateTime; + updatedAt: ISODateTime; +} + +export interface WasmModuleSummary { + id: UUID; + name: string; + description: string | null; + createdAt: ISODateTime; +} + +export interface CreateWasmModuleRequest { + name: string; + description?: string; + witInterface?: string; + // Actual .wasm file uploaded separately via multipart +} + +// ============================================================================= +// Marketplace +// ============================================================================= + +export type MarketplaceListingType = 'tool' | 'flow'; + +export interface MarketplaceListing { + id: UUID; + authorId: UUID; + authorName: string; + type: MarketplaceListingType; + name: string; + description: string; + category: string; + tags: string[]; + downloads: number; + rating: number | null; + content: MarketplaceContent; + createdAt: ISODateTime; + updatedAt: ISODateTime; +} + +export interface MarketplaceListingSummary { + id: UUID; + authorName: string; + type: MarketplaceListingType; + name: string; + description: string; + category: string; + downloads: number; + rating: number | null; +} + +export type MarketplaceContent = ToolContent | FlowTemplateContent; + +export interface ToolContent { + type: 'tool'; + wasmModuleId: UUID; + witInterface: string; + exampleUsage: string; +} + +export interface FlowTemplateContent { + type: 'flow'; + organismYaml: string; + canvasState: CanvasState; +} + +export interface PublishToMarketplaceRequest { + type: MarketplaceListingType; + name: string; + description: string; + category: string; + tags: string[]; + content: MarketplaceContent; +} + +// ============================================================================= +// Project Memory (Pro+, opt-in) +// ============================================================================= + +export interface ProjectMemory { + flowId: UUID; + enabled: boolean; + usedBytes: number; + maxBytes: number; + keys: string[]; +} + +export interface MemoryEntry { + key: string; + value: string; // JSON string + sizeBytes: number; + updatedAt: ISODateTime; +} + +// ============================================================================= +// API Responses +// ============================================================================= + +export interface PaginatedResponse { + items: T[]; + total: number; + page: number; + pageSize: number; + hasMore: boolean; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} + +// ============================================================================= +// Flow Logs (streaming) +// ============================================================================= + +export interface LogEntry { + timestamp: ISODateTime; + level: 'debug' | 'info' | 'warning' | 'error'; + message: string; + metadata?: Record; +} + +// ============================================================================= +// Stats & Analytics +// ============================================================================= + +export interface FlowStats { + flowId: UUID; + executionsTotal: number; + executionsSuccess: number; + executionsError: number; + avgDurationMs: number; + lastExecutedAt: ISODateTime | null; +} + +export interface UsageStats { + userId: UUID; + period: 'day' | 'month'; + flowCount: number; + executionCount: number; + executionLimit: number; +}