|
| 1 | +import crypto from 'crypto' |
| 2 | +import { eq } from 'drizzle-orm' |
| 3 | +import type { NextRequest } from 'next/server' |
| 4 | +import { createLogger } from '@/lib/logs/console-logger' |
| 5 | +import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers' |
| 6 | +import { db } from '@/db' |
| 7 | +import { workflow } from '@/db/schema' |
| 8 | +import type { WorkflowState } from '@/stores/workflows/workflow/types' |
| 9 | +import { validateWorkflowAccess } from '../../middleware' |
| 10 | +import { createErrorResponse, createSuccessResponse } from '../../utils' |
| 11 | + |
| 12 | +const logger = createLogger('RevertToDeployedAPI') |
| 13 | + |
| 14 | +export const dynamic = 'force-dynamic' |
| 15 | +export const runtime = 'nodejs' |
| 16 | + |
| 17 | +/** |
| 18 | + * POST /api/workflows/[id]/revert-to-deployed |
| 19 | + * Revert workflow to its deployed state by saving deployed state to normalized tables |
| 20 | + */ |
| 21 | +export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { |
| 22 | + const requestId = crypto.randomUUID().slice(0, 8) |
| 23 | + const { id } = await params |
| 24 | + |
| 25 | + try { |
| 26 | + logger.debug(`[${requestId}] Reverting workflow to deployed state: ${id}`) |
| 27 | + const validation = await validateWorkflowAccess(request, id, false) |
| 28 | + |
| 29 | + if (validation.error) { |
| 30 | + logger.warn(`[${requestId}] Workflow revert failed: ${validation.error.message}`) |
| 31 | + return createErrorResponse(validation.error.message, validation.error.status) |
| 32 | + } |
| 33 | + |
| 34 | + const workflowData = validation.workflow |
| 35 | + |
| 36 | + // Check if workflow is deployed and has deployed state |
| 37 | + if (!workflowData.isDeployed || !workflowData.deployedState) { |
| 38 | + logger.warn(`[${requestId}] Cannot revert: workflow is not deployed or has no deployed state`) |
| 39 | + return createErrorResponse('Workflow is not deployed or has no deployed state', 400) |
| 40 | + } |
| 41 | + |
| 42 | + // Validate deployed state structure |
| 43 | + const deployedState = workflowData.deployedState as WorkflowState |
| 44 | + if (!deployedState.blocks || !deployedState.edges) { |
| 45 | + logger.error(`[${requestId}] Invalid deployed state structure`, { deployedState }) |
| 46 | + return createErrorResponse('Invalid deployed state structure', 500) |
| 47 | + } |
| 48 | + |
| 49 | + logger.debug(`[${requestId}] Saving deployed state to normalized tables`, { |
| 50 | + blocksCount: Object.keys(deployedState.blocks).length, |
| 51 | + edgesCount: deployedState.edges.length, |
| 52 | + loopsCount: Object.keys(deployedState.loops || {}).length, |
| 53 | + parallelsCount: Object.keys(deployedState.parallels || {}).length, |
| 54 | + }) |
| 55 | + |
| 56 | + // Save deployed state to normalized tables |
| 57 | + const saveResult = await saveWorkflowToNormalizedTables(id, { |
| 58 | + blocks: deployedState.blocks, |
| 59 | + edges: deployedState.edges, |
| 60 | + loops: deployedState.loops || {}, |
| 61 | + parallels: deployedState.parallels || {}, |
| 62 | + lastSaved: Date.now(), |
| 63 | + isDeployed: workflowData.isDeployed, |
| 64 | + deployedAt: workflowData.deployedAt, |
| 65 | + deploymentStatuses: deployedState.deploymentStatuses || {}, |
| 66 | + hasActiveSchedule: deployedState.hasActiveSchedule || false, |
| 67 | + hasActiveWebhook: deployedState.hasActiveWebhook || false, |
| 68 | + }) |
| 69 | + |
| 70 | + if (!saveResult.success) { |
| 71 | + logger.error(`[${requestId}] Failed to save deployed state to normalized tables`, { |
| 72 | + error: saveResult.error, |
| 73 | + }) |
| 74 | + return createErrorResponse( |
| 75 | + saveResult.error || 'Failed to save deployed state to normalized tables', |
| 76 | + 500 |
| 77 | + ) |
| 78 | + } |
| 79 | + |
| 80 | + // Update workflow's last_synced timestamp to indicate changes |
| 81 | + await db |
| 82 | + .update(workflow) |
| 83 | + .set({ |
| 84 | + lastSynced: new Date(), |
| 85 | + updatedAt: new Date(), |
| 86 | + }) |
| 87 | + .where(eq(workflow.id, id)) |
| 88 | + |
| 89 | + // Notify socket server about the revert operation for real-time sync |
| 90 | + try { |
| 91 | + const socketServerUrl = process.env.SOCKET_SERVER_URL || 'http://localhost:3002' |
| 92 | + await fetch(`${socketServerUrl}/api/workflow-reverted`, { |
| 93 | + method: 'POST', |
| 94 | + headers: { |
| 95 | + 'Content-Type': 'application/json', |
| 96 | + }, |
| 97 | + body: JSON.stringify({ |
| 98 | + workflowId: id, |
| 99 | + timestamp: Date.now(), |
| 100 | + }), |
| 101 | + }) |
| 102 | + logger.debug(`[${requestId}] Notified socket server about workflow revert: ${id}`) |
| 103 | + } catch (socketError) { |
| 104 | + // Don't fail the request if socket notification fails |
| 105 | + logger.warn(`[${requestId}] Failed to notify socket server about revert:`, socketError) |
| 106 | + } |
| 107 | + |
| 108 | + logger.info(`[${requestId}] Successfully reverted workflow to deployed state: ${id}`) |
| 109 | + |
| 110 | + return createSuccessResponse({ |
| 111 | + message: 'Workflow successfully reverted to deployed state', |
| 112 | + lastSaved: Date.now(), |
| 113 | + }) |
| 114 | + } catch (error: any) { |
| 115 | + logger.error(`[${requestId}] Error reverting workflow to deployed state: ${id}`, { |
| 116 | + error: error.message, |
| 117 | + stack: error.stack, |
| 118 | + }) |
| 119 | + return createErrorResponse(error.message || 'Failed to revert workflow to deployed state', 500) |
| 120 | + } |
| 121 | +} |
0 commit comments