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 <noreply@anthropic.com>
This commit is contained in:
dullfig 2026-01-20 22:16:20 -08:00
parent a6cd3ce7e8
commit 03516bb9f7
2 changed files with 815 additions and 0 deletions

View file

@ -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

View file

@ -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<string, unknown>;
}
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<T> {
items: T[];
total: number;
page: number;
pageSize: number;
hasMore: boolean;
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, unknown>;
}
// =============================================================================
// Flow Logs (streaming)
// =============================================================================
export interface LogEntry {
timestamp: ISODateTime;
level: 'debug' | 'info' | 'warning' | 'error';
message: string;
metadata?: Record<string, unknown>;
}
// =============================================================================
// 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;
}