chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:37:47 +08:00
commit 7653f56fed
1422 changed files with 359026 additions and 0 deletions
@@ -0,0 +1,76 @@
import { NextRequest, NextResponse } from 'next/server';
import { LangGraphExecutor } from '@/lib/workflow/langgraph';
import { getWorkflow } from '@/lib/workflow/storage';
import { getServerAPIKeys } from '@/lib/api/config';
import { validateApiKey } from '@/lib/api/auth';
export const dynamic = 'force-dynamic';
/**
* Execute workflow using LangGraph
* POST /api/workflows/:workflowId/execute-langgraph
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ workflowId: string }> }
) {
try {
// Validate API key
const authResult = await validateApiKey(request);
if (!authResult.authenticated) {
return NextResponse.json(
{ error: authResult.error || 'Authentication required' },
{ status: 401 }
);
}
const { workflowId } = await params;
const body = await request.json();
const { input, threadId } = body;
// Load workflow
const workflow = await getWorkflow(workflowId);
if (!workflow) {
return NextResponse.json(
{ error: 'Workflow not found' },
{ status: 404 }
);
}
// Get API keys - check user keys first, then fall back to environment
const { getLLMApiKey } = await import('@/lib/api/llm-keys');
const userId = authResult.userId;
const apiKeys = {
anthropic: userId ? await getLLMApiKey('anthropic', userId) : null || process.env.ANTHROPIC_API_KEY,
groq: userId ? await getLLMApiKey('groq', userId) : null || process.env.GROQ_API_KEY,
openai: userId ? await getLLMApiKey('openai', userId) : null || process.env.OPENAI_API_KEY,
firecrawl: process.env.FIRECRAWL_API_KEY, // Firecrawl keys are still environment-only for now
arcade: process.env.ARCADE_API_KEY,
};
// Create LangGraph executor
const executor = new LangGraphExecutor(workflow, undefined, apiKeys || undefined);
// Execute workflow
const result = await executor.execute(input, { threadId });
return NextResponse.json({
success: true,
executionId: result.id,
status: result.status,
nodeResults: result.nodeResults,
startedAt: result.startedAt,
completedAt: result.completedAt,
});
} catch (error) {
console.error('LangGraph execution error:', error);
return NextResponse.json(
{
error: 'Workflow execution failed',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}
@@ -0,0 +1,277 @@
import { NextRequest } from 'next/server';
import { getConvexClient, getAuthenticatedConvexClient, api, isConvexConfigured } from '@/lib/convex/client';
import { LangGraphExecutor } from '@/lib/workflow/langgraph';
import { validateApiKey, createUnauthorizedResponse } from '@/lib/api/auth';
export const dynamic = 'force-dynamic';
/**
* Streaming workflow execution with real-time updates
* Uses Server-Sent Events (SSE) to stream node execution progress
*
* Uses LangGraph executor for state management with Convex storage
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ workflowId: string }> }
) {
// Validate API key
const authResult = await validateApiKey(request);
if (!authResult.authenticated) {
return createUnauthorizedResponse(authResult.error || 'Authentication required');
}
const { workflowId } = await params;
// Create SSE stream
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const sendEvent = (event: string, data: any) => {
try {
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
controller.enqueue(encoder.encode(message));
} catch (error) {
console.error('Failed to send SSE event:', error);
}
};
try {
// Get inputs from request body
const body = await request.json();
const inputs = body || {};
// Get workflow from Convex
if (!isConvexConfigured()) {
sendEvent('error', {
error: 'Convex not configured',
workflowId,
});
controller.close();
return;
}
const convex = await getAuthenticatedConvexClient();
// Look up workflow - try customId first, then try as Convex ID
let workflowDoc = await convex.query(api.workflows.getWorkflowByCustomId, {
customId: workflowId,
});
// If not found by customId and looks like Convex ID, try direct lookup
if (!workflowDoc && workflowId.startsWith('j')) {
try {
workflowDoc = await convex.query(api.workflows.getWorkflow, {
id: workflowId as any,
});
} catch (e) {
// Not a valid Convex ID
}
}
if (!workflowDoc) {
sendEvent('error', {
error: `Workflow ${workflowId} not found`,
workflowId,
});
controller.close();
return;
}
// Convert Convex document to workflow format
const workflowData = {
...workflowDoc,
id: workflowDoc.customId || workflowDoc._id, // Use customId if exists, otherwise Convex ID
};
if (!workflowData) {
sendEvent('error', {
error: `Workflow ${workflowId} not found`,
workflowId,
});
controller.close();
return;
}
const workflow = workflowData as any;
// Send start event
sendEvent('workflow_started', {
workflowId,
workflowName: workflow.name,
totalNodes: workflow.nodes.length,
timestamp: new Date().toISOString(),
});
// Create a custom execution with progress callbacks
const executionId = `exec_${Date.now()}`;
const nodeResults: Record<string, any> = {};
// Get API keys - check user keys first, then fall back to environment
const { getLLMApiKey } = await import('@/lib/api/llm-keys');
const userId = authResult.userId;
const apiKeys = {
anthropic: userId ? await getLLMApiKey('anthropic', userId) : null || process.env.ANTHROPIC_API_KEY,
groq: userId ? await getLLMApiKey('groq', userId) : null || process.env.GROQ_API_KEY,
openai: userId ? await getLLMApiKey('openai', userId) : null || process.env.OPENAI_API_KEY,
firecrawl: process.env.FIRECRAWL_API_KEY, // Firecrawl keys are still environment-only for now
arcade: process.env.ARCADE_API_KEY,
};
// Prepare initial input - pass as object if it's an object, otherwise as string
let initialInput: any = '';
if (typeof inputs === 'object' && Object.keys(inputs).length > 0) {
// If the body has an "input" field, extract it (common pattern from curl/API calls)
// Otherwise use the body directly
initialInput = inputs.input || inputs;
} else {
// Otherwise use url or input field
initialInput = inputs.url || inputs.input || '';
}
// LangGraph Execution Path
const threadId = `thread_${workflowId}_${Date.now()}`;
let executor;
try {
executor = new LangGraphExecutor(
workflow,
(nodeId, result) => {
nodeResults[nodeId] = result;
if (result.status === 'running') {
const node = workflow.nodes.find((n: any) => n.id === nodeId);
sendEvent('node_started', {
nodeId,
nodeName: node?.data?.nodeName || node?.data?.label || nodeId,
nodeType: node?.type || 'unknown',
timestamp: new Date().toISOString(),
});
} else if (result.status === 'completed') {
const node = workflow.nodes.find((n: any) => n.id === nodeId);
sendEvent('node_completed', {
nodeId,
nodeName: node?.data?.nodeName || node?.data?.label || nodeId,
result,
timestamp: new Date().toISOString(),
});
} else if (result.status === 'failed') {
const node = workflow.nodes.find((n: any) => n.id === nodeId);
sendEvent('node_failed', {
nodeId,
nodeName: node?.data?.nodeName || node?.data?.label || nodeId,
error: result.error,
timestamp: new Date().toISOString(),
});
} else if (result.status === 'pending-authorization' || result.status === 'pending-approval') {
const node = workflow.nodes.find((n: any) => n.id === nodeId);
sendEvent('node_paused', {
nodeId,
nodeName: node?.data?.nodeName || node?.data?.label || nodeId,
status: result.status,
timestamp: new Date().toISOString(),
});
}
},
apiKeys
);
} catch (graphBuildError) {
console.error('❌ Failed to build LangGraph:', graphBuildError);
sendEvent('error', {
error: graphBuildError instanceof Error ? graphBuildError.message : 'Graph compilation failed',
timestamp: new Date().toISOString(),
});
controller.close();
return;
}
// Execute with streaming
const executionStream = await executor.executeStream(initialInput, {
threadId,
executionId,
});
let finalState: any = null;
// CRITICAL FIX: Proper async iteration with error handling
try {
for await (const stateUpdate of executionStream) {
const mergedState = {
...stateUpdate,
nodeResults: {
...stateUpdate.nodeResults,
...nodeResults,
},
};
finalState = mergedState;
sendEvent('state_update', {
nodeResults: mergedState.nodeResults,
currentNodeId: mergedState.currentNodeId,
pendingAuth: mergedState.pendingAuth,
timestamp: new Date().toISOString(),
});
// Check for pending auth/approval
if (mergedState.pendingAuth) {
sendEvent('workflow_paused', {
reason: 'pending_authorization',
pendingAuth: mergedState.pendingAuth,
executionId,
threadId,
timestamp: new Date().toISOString(),
});
// TODO: Save execution state to Convex for resume capability
// await convex.mutation(api.executions.createExecution, {...})
controller.close();
return;
}
}
} catch (streamError) {
console.error('Stream iteration error:', streamError);
sendEvent('error', {
error: streamError instanceof Error ? streamError.message : 'Stream error',
timestamp: new Date().toISOString(),
});
controller.close();
return;
}
// Send completion event
const status = finalState?.pendingAuth ? 'waiting-auth' : 'completed';
sendEvent('workflow_completed', {
workflowId,
executionId,
results: finalState?.nodeResults || {},
status,
timestamp: new Date().toISOString(),
});
// TODO: Save execution results to Convex
// await convex.mutation(api.executions.completeExecution, {...})
controller.close();
} catch (error) {
sendEvent('error', {
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
});
controller.close();
}
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // Disable nginx buffering
},
});
}
@@ -0,0 +1,63 @@
import { NextRequest, NextResponse } from 'next/server';
import { LangGraphExecutor } from '@/lib/workflow/langgraph';
import { validateApiKey, createUnauthorizedResponse } from '@/lib/api/auth';
export const dynamic = 'force-dynamic';
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ workflowId: string }> }
) {
// Validate API key
const authResult = await validateApiKey(request);
if (!authResult.authenticated) {
return createUnauthorizedResponse(authResult.error || 'Authentication required');
}
try {
const { workflowId } = await params;
const body = await request.json();
const { input, workflow } = body;
console.log('API: Executing workflow', workflowId, 'with input:', input);
if (!workflow || !workflow.nodes) {
return NextResponse.json(
{ error: 'Workflow data is required in request body' },
{ status: 400 }
);
}
console.log('API: Loaded workflow:', workflow.name);
const apiKeys = {
anthropic: process.env.ANTHROPIC_API_KEY,
groq: process.env.GROQ_API_KEY,
openai: process.env.OPENAI_API_KEY,
firecrawl: process.env.FIRECRAWL_API_KEY,
arcade: process.env.ARCADE_API_KEY,
};
// Execute workflow using LangGraph
const executor = new LangGraphExecutor(workflow, undefined, apiKeys);
const execution = await executor.execute(input || '');
console.log('API: Execution complete:', execution.status);
return NextResponse.json({
success: execution.status === 'completed',
execution,
input,
workflowName: workflow.name,
});
} catch (error) {
console.error('Workflow execution error:', error);
return NextResponse.json(
{
error: 'Workflow execution failed',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}
@@ -0,0 +1,45 @@
import { NextRequest, NextResponse } from 'next/server';
import { workflowToLangGraphCode } from '@/lib/workflow/langgraph';
export const dynamic = 'force-dynamic';
/**
* Export workflow as executable LangGraph TypeScript code
* POST /api/workflows/:workflowId/export-code
* Expects workflow data in request body
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ workflowId: string }> }
) {
try {
const { workflowId } = await params;
const workflow = await request.json();
if (!workflow || !workflow.nodes) {
return NextResponse.json(
{ error: 'Workflow data is required in request body' },
{ status: 400 }
);
}
// Generate TypeScript code
const code = workflowToLangGraphCode(workflow);
// Return as JSON with code string
return NextResponse.json({
code,
filename: `${workflow.name.replace(/\s+/g, '_')}.ts`,
language: 'typescript',
});
} catch (error) {
console.error('Code export error:', error);
return NextResponse.json(
{
error: 'Code export failed',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}
@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server';
import { workflowToLangGraphJSON } from '@/lib/workflow/langgraph';
export const dynamic = 'force-dynamic';
/**
* Export workflow as LangGraph JSON
* POST /api/workflows/:workflowId/export-langgraph
* Expects workflow data in request body
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ workflowId: string }> }
) {
try {
const { workflowId } = await params;
const workflow = await request.json();
if (!workflow || !workflow.nodes) {
return NextResponse.json(
{ error: 'Workflow data is required in request body' },
{ status: 400 }
);
}
// Convert to LangGraph format
const langGraphJSON = workflowToLangGraphJSON(workflow);
// Return as downloadable JSON
return new NextResponse(JSON.stringify(langGraphJSON, null, 2), {
headers: {
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="${workflow.name.replace(/\s+/g, '_')}_langgraph.json"`,
},
});
} catch (error) {
console.error('Export error:', error);
return NextResponse.json(
{
error: 'Export failed',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}
@@ -0,0 +1,219 @@
import { NextRequest } from 'next/server';
import { getConvexClient, getAuthenticatedConvexClient, api, isConvexConfigured } from '@/lib/convex/client';
import { LangGraphExecutor } from '@/lib/workflow/langgraph';
import { validateApiKey, createUnauthorizedResponse } from '@/lib/api/auth';
export const dynamic = 'force-dynamic';
/**
* Resume a paused workflow execution
* Uses LangGraph's resumeFromAuth to continue from interrupt point
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ workflowId: string }> }
) {
// Validate API key
const authResult = await validateApiKey(request);
if (!authResult.authenticated) {
return createUnauthorizedResponse(authResult.error || 'Authentication required');
}
const { workflowId } = await params;
// Create SSE stream
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const sendEvent = (event: string, data: any) => {
try {
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
controller.enqueue(encoder.encode(message));
} catch (error) {
console.error('Failed to send SSE event:', error);
}
};
try {
// Get resume data from request
const body = await request.json();
const { threadId, resumeValue, executionId } = body;
if (!threadId) {
sendEvent('error', { error: 'threadId is required for resume' });
controller.close();
return;
}
// Get workflow from Convex
if (!isConvexConfigured()) {
sendEvent('error', { error: 'Convex not configured' });
controller.close();
return;
}
const convex = await getAuthenticatedConvexClient();
// Look up workflow
let workflowDoc = await convex.query(api.workflows.getWorkflowByCustomId, {
customId: workflowId,
});
if (!workflowDoc && workflowId.startsWith('j')) {
try {
workflowDoc = await convex.query(api.workflows.getWorkflow, {
id: workflowId as any,
});
} catch (e) {
// Not a valid Convex ID
}
}
if (!workflowDoc) {
sendEvent('error', { error: `Workflow ${workflowId} not found` });
controller.close();
return;
}
const workflow = {
...workflowDoc,
id: workflowDoc.customId || workflowDoc._id,
} as any;
// Get API keys - check user keys first, then fall back to environment
const { getLLMApiKey } = await import('@/lib/api/llm-keys');
const userId = authResult.userId;
const apiKeys = {
anthropic: userId ? await getLLMApiKey('anthropic', userId) : null || process.env.ANTHROPIC_API_KEY,
groq: userId ? await getLLMApiKey('groq', userId) : null || process.env.GROQ_API_KEY,
openai: userId ? await getLLMApiKey('openai', userId) : null || process.env.OPENAI_API_KEY,
firecrawl: process.env.FIRECRAWL_API_KEY, // Firecrawl keys are still environment-only for now
arcade: process.env.ARCADE_API_KEY,
};
const nodeResults: Record<string, any> = {};
// Create executor
const executor = new LangGraphExecutor(
workflow,
(nodeId, result) => {
nodeResults[nodeId] = result;
if (result.status === 'running') {
const node = workflow.nodes.find((n: any) => n.id === nodeId);
sendEvent('node_started', {
nodeId,
nodeName: node?.data?.nodeName || node?.data?.label || nodeId,
nodeType: node?.type || 'unknown',
timestamp: new Date().toISOString(),
});
} else if (result.status === 'completed') {
const node = workflow.nodes.find((n: any) => n.id === nodeId);
sendEvent('node_completed', {
nodeId,
nodeName: node?.data?.nodeName || node?.data?.label || nodeId,
result,
timestamp: new Date().toISOString(),
});
} else if (result.status === 'failed') {
const node = workflow.nodes.find((n: any) => n.id === nodeId);
sendEvent('node_failed', {
nodeId,
nodeName: node?.data?.nodeName || node?.data?.label || nodeId,
error: result.error,
timestamp: new Date().toISOString(),
});
}
},
apiKeys
);
// Resume execution from pause point
const resumeStream = await executor.resumeFromAuth(
threadId,
resumeValue || { approved: true, status: 'approved' },
{ executionId }
);
sendEvent('workflow_resumed', {
threadId,
executionId,
timestamp: new Date().toISOString(),
});
let finalState: any = null;
// Stream resumed execution
try {
for await (const stateUpdate of resumeStream) {
const mergedState = {
...stateUpdate,
nodeResults: {
...stateUpdate.nodeResults,
...nodeResults,
},
};
finalState = mergedState;
sendEvent('state_update', {
nodeResults: mergedState.nodeResults,
currentNodeId: mergedState.currentNodeId,
pendingAuth: mergedState.pendingAuth,
timestamp: new Date().toISOString(),
});
// Check for another pending auth/approval
if (mergedState.pendingAuth) {
sendEvent('workflow_paused', {
reason: 'pending_authorization',
pendingAuth: mergedState.pendingAuth,
executionId,
threadId,
timestamp: new Date().toISOString(),
});
controller.close();
return;
}
}
} catch (streamError) {
console.error('Resume stream error:', streamError);
sendEvent('error', {
error: streamError instanceof Error ? streamError.message : 'Stream error',
timestamp: new Date().toISOString(),
});
controller.close();
return;
}
// Send completion event
sendEvent('workflow_completed', {
workflowId,
executionId,
results: finalState?.nodeResults || {},
status: 'completed',
timestamp: new Date().toISOString(),
});
controller.close();
} catch (error) {
sendEvent('error', {
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
});
controller.close();
}
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no',
},
});
}
@@ -0,0 +1,119 @@
import { NextRequest, NextResponse } from 'next/server';
import { getConvexClient, getAuthenticatedConvexClient, api, isConvexConfigured } from '@/lib/convex/client';
export const dynamic = 'force-dynamic';
/**
* GET /api/workflows/[workflowId] - Get a specific workflow from Convex
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ workflowId: string }> }
) {
try {
const { workflowId } = await params;
if (!isConvexConfigured()) {
return NextResponse.json(
{ error: 'Convex not configured' },
{ status: 500 }
);
}
const convex = await getAuthenticatedConvexClient();
// Look up by customId first, then try as Convex ID
let workflow = await convex.query(api.workflows.getWorkflowByCustomId, {
customId: workflowId,
});
// If not found and looks like Convex ID, try direct lookup
if (!workflow && workflowId.startsWith('j')) {
try {
workflow = await convex.query(api.workflows.getWorkflow, {
id: workflowId as any,
});
} catch (e) {
// Not a valid Convex ID
}
}
if (!workflow) {
return NextResponse.json(
{ error: `Workflow ${workflowId} not found` },
{ status: 404 }
);
}
return NextResponse.json({
success: true,
workflow: {
...workflow,
id: workflow.customId || workflow._id, // Return customId if exists
},
source: 'convex',
});
} catch (error) {
console.error('Error fetching workflow:', error);
return NextResponse.json(
{
error: 'Failed to fetch workflow',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}
/**
* DELETE /api/workflows/[workflowId] - Delete a workflow from Convex
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ workflowId: string }> }
) {
try {
const { workflowId } = await params;
if (!isConvexConfigured()) {
return NextResponse.json(
{ error: 'Convex not configured' },
{ status: 500 }
);
}
const convex = await getAuthenticatedConvexClient();
// Look up by customId to get Convex ID
const workflow = await convex.query(api.workflows.getWorkflowByCustomId, {
customId: workflowId,
});
if (!workflow) {
return NextResponse.json(
{ error: `Workflow ${workflowId} not found` },
{ status: 404 }
);
}
// Delete using Convex ID
await convex.mutation(api.workflows.deleteWorkflow, {
id: workflow._id,
});
return NextResponse.json({
success: true,
source: 'convex',
message: `Workflow ${workflowId} deleted`,
});
} catch (error) {
console.error('Error deleting workflow:', error);
return NextResponse.json(
{
error: 'Failed to delete workflow',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}
@@ -0,0 +1,24 @@
import { NextResponse } from 'next/server';
import { getAuthenticatedConvexClient, api } from '@/lib/convex/client';
/**
* DELETE /api/workflows/cleanup
* Clean up workflows without userId (development/admin only)
*/
export async function DELETE() {
try {
const convex = await getAuthenticatedConvexClient();
const result = await convex.mutation(api.workflows.deleteWorkflowsWithoutUserId, {});
return NextResponse.json(result);
} catch (error) {
console.error('Error cleaning up workflows:', error);
return NextResponse.json(
{
error: 'Failed to clean up workflows',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}
@@ -0,0 +1,49 @@
import { NextRequest, NextResponse } from 'next/server';
import { langGraphJSONToWorkflow } from '@/lib/workflow/langgraph';
import { saveWorkflow } from '@/lib/workflow/storage';
export const dynamic = 'force-dynamic';
/**
* Import workflow from LangGraph JSON
* POST /api/workflows/import-langgraph
*/
export async function POST(request: NextRequest) {
try {
const langGraphJSON = await request.json();
// Validate input
if (!langGraphJSON.nodes || !langGraphJSON.edges) {
return NextResponse.json(
{ error: 'Invalid LangGraph JSON: missing nodes or edges' },
{ status: 400 }
);
}
// Convert to workflow format
const workflow = langGraphJSONToWorkflow(langGraphJSON);
// Save workflow
await saveWorkflow(workflow);
return NextResponse.json({
success: true,
workflow: {
id: workflow.id,
name: workflow.name,
description: workflow.description,
nodeCount: workflow.nodes.length,
edgeCount: workflow.edges.length,
},
});
} catch (error) {
console.error('Import error:', error);
return NextResponse.json(
{
error: 'Import failed',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}
@@ -0,0 +1,212 @@
import { NextRequest, NextResponse } from 'next/server';
import { getConvexClient, getAuthenticatedConvexClient, api, isConvexConfigured } from '@/lib/convex/client';
export const dynamic = 'force-dynamic';
/**
* GET /api/workflows - List all workflows
* Uses Convex for storage
*/
export async function GET(request: NextRequest) {
try {
if (!isConvexConfigured()) {
return NextResponse.json({
workflows: [],
total: 0,
source: 'none',
message: 'Convex not configured. Add NEXT_PUBLIC_CONVEX_URL to .env.local',
});
}
const convex = await getAuthenticatedConvexClient();
const workflows = await convex.query(api.workflows.listWorkflows, {});
return NextResponse.json({
workflows: workflows.map((w: any) => ({
id: w.customId || w._id, // Use customId if exists, otherwise Convex ID
name: w.name,
description: w.description,
category: w.category,
tags: w.tags,
difficulty: w.difficulty,
estimatedTime: w.estimatedTime,
nodes: w.nodes,
edges: w.edges,
createdAt: w.createdAt,
updatedAt: w.updatedAt,
nodeCount: w.nodes?.length || 0,
edgeCount: w.edges?.length || 0,
})),
total: workflows.length,
source: 'convex',
});
} catch (error) {
console.error('Error fetching workflows:', error);
return NextResponse.json(
{
error: 'Failed to fetch workflows',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}
/**
* POST /api/workflows - Save a workflow to Convex
*/
export async function POST(request: NextRequest) {
try {
let workflow;
try {
const body = await request.text();
if (!body || body.trim() === '') {
return NextResponse.json(
{ error: 'Request body is empty' },
{ status: 400 }
);
}
workflow = JSON.parse(body);
} catch (parseError) {
console.error('JSON parse error:', parseError);
return NextResponse.json(
{ error: 'Invalid JSON in request body' },
{ status: 400 }
);
}
if (!workflow.id && !workflow.name) {
return NextResponse.json(
{ error: 'Workflow must have either id or name' },
{ status: 400 }
);
}
if (!isConvexConfigured()) {
return NextResponse.json({
success: false,
message: 'Convex not configured. Add NEXT_PUBLIC_CONVEX_URL to .env.local',
}, { status: 500 });
}
// Validate workflow has required fields
if (!workflow.nodes || !Array.isArray(workflow.nodes)) {
return NextResponse.json(
{ error: 'Workflow must have a nodes array' },
{ status: 400 }
);
}
if (!workflow.edges || !Array.isArray(workflow.edges)) {
return NextResponse.json(
{ error: 'Workflow must have an edges array' },
{ status: 400 }
);
}
const convex = await getAuthenticatedConvexClient();
// Use workflow.id as customId for Convex
const customId = workflow.id || `workflow_${Date.now()}`;
const savedId = await convex.mutation(api.workflows.saveWorkflow, {
customId,
name: workflow.name || 'Untitled Workflow',
description: workflow.description,
category: workflow.category,
tags: workflow.tags,
difficulty: workflow.difficulty,
estimatedTime: workflow.estimatedTime,
nodes: workflow.nodes,
edges: workflow.edges,
version: workflow.version,
isTemplate: workflow.isTemplate,
});
return NextResponse.json({
success: true,
workflowId: savedId,
source: 'convex',
message: 'Workflow saved successfully',
});
} catch (error) {
console.error('Error saving workflow:', error);
return NextResponse.json(
{
error: 'Failed to save workflow',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}
/**
* DELETE /api/workflows?id=xxx - Delete a workflow from Convex
*/
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const workflowId = searchParams.get('id');
if (!workflowId) {
return NextResponse.json(
{ error: 'Workflow ID is required' },
{ status: 400 }
);
}
if (!isConvexConfigured()) {
return NextResponse.json({
success: false,
message: 'Convex not configured',
}, { status: 500 });
}
const convex = await getAuthenticatedConvexClient();
// Look up workflow by customId first, then try Convex ID
let workflow = await convex.query(api.workflows.getWorkflowByCustomId, {
customId: workflowId,
});
// If not found and looks like Convex ID, try direct lookup
if (!workflow && workflowId.startsWith('j')) {
try {
workflow = await convex.query(api.workflows.getWorkflow, {
id: workflowId as any,
});
} catch (e) {
// Not a valid Convex ID
}
}
if (!workflow) {
return NextResponse.json(
{ error: `Workflow ${workflowId} not found` },
{ status: 404 }
);
}
// Delete using Convex ID
await convex.mutation(api.workflows.deleteWorkflow, {
id: workflow._id,
});
return NextResponse.json({
success: true,
source: 'convex',
message: 'Workflow deleted successfully',
});
} catch (error) {
console.error('Error deleting workflow:', error);
return NextResponse.json(
{
error: 'Failed to delete workflow',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}