208 lines
6.4 KiB
TypeScript
208 lines
6.4 KiB
TypeScript
import { defineSchema, defineTable } from "convex/server";
|
|
import { v } from "convex/values";
|
|
|
|
/**
|
|
* Convex Schema for Open Agent Builder
|
|
*
|
|
* Replaces Upstash Redis for workflow storage
|
|
*/
|
|
|
|
export default defineSchema({
|
|
// Users table - synced from Clerk
|
|
users: defineTable({
|
|
clerkId: v.string(),
|
|
email: v.optional(v.string()),
|
|
name: v.optional(v.string()),
|
|
imageUrl: v.optional(v.string()),
|
|
createdAt: v.string(),
|
|
})
|
|
.index("by_clerkId", ["clerkId"])
|
|
.index("by_email", ["email"]),
|
|
|
|
// Workflows table - stores complete workflow definitions
|
|
workflows: defineTable({
|
|
// User ownership
|
|
userId: v.optional(v.string()), // Clerk user ID - optional for backward compat
|
|
|
|
// Workflow identification
|
|
customId: v.optional(v.string()), // Original workflow ID (like "workflow_123" or "amazon-product-research")
|
|
name: v.string(),
|
|
description: v.optional(v.string()),
|
|
category: v.optional(v.string()),
|
|
tags: v.optional(v.array(v.string())),
|
|
difficulty: v.optional(v.string()),
|
|
estimatedTime: v.optional(v.string()),
|
|
|
|
// Workflow structure
|
|
nodes: v.array(v.any()), // Workflow nodes with flexible structure
|
|
edges: v.array(v.any()), // Workflow edges
|
|
|
|
// Timestamps
|
|
createdAt: v.string(),
|
|
updatedAt: v.string(),
|
|
|
|
// Optional metadata
|
|
version: v.optional(v.string()),
|
|
isTemplate: v.optional(v.boolean()),
|
|
isPublic: v.optional(v.boolean()), // For shared templates
|
|
})
|
|
.index("by_userId", ["userId"])
|
|
.index("by_customId", ["customId"])
|
|
.index("by_creation", ["createdAt"])
|
|
.index("by_category", ["category"])
|
|
.index("by_template", ["isTemplate"]),
|
|
|
|
// Workflow executions - track execution state
|
|
executions: defineTable({
|
|
workflowId: v.id("workflows"),
|
|
status: v.string(), // "running" | "completed" | "failed"
|
|
|
|
// Execution state
|
|
currentNodeId: v.optional(v.string()),
|
|
nodeResults: v.any(), // Flexible execution results
|
|
variables: v.any(), // State variables
|
|
|
|
// Input/Output
|
|
input: v.optional(v.any()),
|
|
output: v.optional(v.any()),
|
|
error: v.optional(v.string()),
|
|
|
|
// Timestamps
|
|
startedAt: v.string(),
|
|
completedAt: v.optional(v.string()),
|
|
|
|
// Execution metadata
|
|
threadId: v.optional(v.string()),
|
|
})
|
|
.index("by_workflow", ["workflowId"])
|
|
.index("by_status", ["status"])
|
|
.index("by_started", ["startedAt"]),
|
|
|
|
// MCP servers registry - Centralized configuration
|
|
mcpServers: defineTable({
|
|
// Ownership
|
|
userId: v.string(), // Clerk user ID who owns this MCP
|
|
|
|
// Basic info
|
|
name: v.string(),
|
|
url: v.string(),
|
|
description: v.optional(v.string()),
|
|
category: v.string(), // "web" | "ai" | "data" | "custom"
|
|
|
|
// Authentication
|
|
authType: v.string(), // "none" | "api-key" | "bearer" | "oauth-coming-soon"
|
|
accessToken: v.optional(v.string()), // Encrypted token
|
|
|
|
// Tools & Status
|
|
tools: v.optional(v.array(v.string())), // List of available tool names
|
|
connectionStatus: v.string(), // "connected" | "error" | "untested"
|
|
lastTested: v.optional(v.string()),
|
|
lastError: v.optional(v.string()),
|
|
|
|
// Configuration
|
|
enabled: v.boolean(),
|
|
isOfficial: v.boolean(), // Built-in MCPs from registry
|
|
|
|
// Headers for custom config
|
|
headers: v.optional(v.any()),
|
|
|
|
createdAt: v.string(),
|
|
updatedAt: v.string(),
|
|
})
|
|
.index("by_userId", ["userId"])
|
|
.index("by_name", ["name"])
|
|
.index("by_enabled", ["enabled"])
|
|
.index("by_category", ["category"])
|
|
.index("by_official", ["isOfficial"]),
|
|
|
|
// Arcade auth records
|
|
arcadeAuth: defineTable({
|
|
authId: v.string(),
|
|
toolName: v.string(),
|
|
authUrl: v.optional(v.string()),
|
|
userId: v.optional(v.string()),
|
|
status: v.string(), // "pending" | "completed" | "failed"
|
|
|
|
createdAt: v.string(),
|
|
completedAt: v.optional(v.string()),
|
|
})
|
|
.index("by_authId", ["authId"])
|
|
.index("by_status", ["status"]),
|
|
|
|
// User MCP Servers - Cursor-style configuration
|
|
userMCPs: defineTable({
|
|
userId: v.string(),
|
|
name: v.string(),
|
|
url: v.string(),
|
|
headers: v.optional(v.any()),
|
|
env: v.optional(v.any()),
|
|
createdAt: v.string(),
|
|
updatedAt: v.string(),
|
|
})
|
|
.index("by_userId", ["userId"])
|
|
.index("by_name", ["name"]),
|
|
|
|
// API Keys - User-generated keys for API access
|
|
apiKeys: defineTable({
|
|
key: v.string(), // Hashed key
|
|
keyPrefix: v.string(), // "sk_live_abc..." for display
|
|
userId: v.string(), // Clerk user ID who owns this key
|
|
name: v.string(), // User-given name
|
|
|
|
usageCount: v.number(),
|
|
lastUsedAt: v.optional(v.string()),
|
|
|
|
createdAt: v.string(),
|
|
expiresAt: v.optional(v.string()),
|
|
revokedAt: v.optional(v.string()),
|
|
})
|
|
.index("by_userId", ["userId"])
|
|
.index("by_key", ["key"])
|
|
.index("by_keyPrefix", ["keyPrefix"]),
|
|
|
|
// User LLM API Keys - Store user's own LLM provider keys
|
|
userLLMKeys: defineTable({
|
|
userId: v.string(), // Clerk user ID
|
|
provider: v.string(), // "anthropic" | "openai" | "groq"
|
|
encryptedKey: v.string(), // Encrypted API key
|
|
keyPrefix: v.string(), // First/last few chars for display (e.g. "sk-ant...abc")
|
|
|
|
// Metadata
|
|
label: v.optional(v.string()), // User-friendly label
|
|
isActive: v.boolean(), // Whether this key is currently active
|
|
|
|
// Timestamps
|
|
createdAt: v.string(),
|
|
updatedAt: v.string(),
|
|
lastUsedAt: v.optional(v.string()),
|
|
|
|
// Usage tracking (optional)
|
|
usageCount: v.optional(v.number()),
|
|
})
|
|
.index("by_userId", ["userId"])
|
|
.index("by_provider", ["provider"])
|
|
.index("by_userProvider", ["userId", "provider"])
|
|
.index("by_active", ["isActive"]),
|
|
|
|
// Approval records - Human-in-the-loop workflow pauses
|
|
approvals: defineTable({
|
|
approvalId: v.string(),
|
|
workflowId: v.id("workflows"),
|
|
executionId: v.optional(v.string()), // Which execution is waiting
|
|
nodeId: v.optional(v.string()), // Which node is waiting for approval
|
|
message: v.string(),
|
|
status: v.string(), // "pending" | "approved" | "rejected"
|
|
userId: v.optional(v.string()), // Who needs to approve (Clerk user ID)
|
|
createdBy: v.optional(v.string()), // Who created the approval request
|
|
|
|
createdAt: v.string(),
|
|
respondedAt: v.optional(v.string()),
|
|
respondedBy: v.optional(v.string()), // Who responded (Clerk user ID)
|
|
})
|
|
.index("by_approvalId", ["approvalId"])
|
|
.index("by_status", ["status"])
|
|
.index("by_userId", ["userId"])
|
|
.index("by_workflow", ["workflowId"])
|
|
.index("by_execution", ["executionId"]),
|
|
});
|