Skip to content

Commit 3e45d79

Browse files
icecrasher321Vikhyath Mondreti
andauthored
fix(revert-deployed): correctly revert to deployed state as unit op using separate endpoint (#633)
* fix(revert-deployed): revert deployed functionality with separate endpoint * fix lint --------- Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net>
1 parent 5167deb commit 3e45d79

File tree

6 files changed

+277
-9
lines changed

6 files changed

+277
-9
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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+
}

apps/sim/contexts/socket-context.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ interface SocketContextType {
5050
onUserJoined: (handler: (data: any) => void) => void
5151
onUserLeft: (handler: (data: any) => void) => void
5252
onWorkflowDeleted: (handler: (data: any) => void) => void
53+
onWorkflowReverted: (handler: (data: any) => void) => void
5354
}
5455

5556
const SocketContext = createContext<SocketContextType>({
@@ -71,6 +72,7 @@ const SocketContext = createContext<SocketContextType>({
7172
onUserJoined: () => {},
7273
onUserLeft: () => {},
7374
onWorkflowDeleted: () => {},
75+
onWorkflowReverted: () => {},
7476
})
7577

7678
export const useSocket = () => useContext(SocketContext)
@@ -100,6 +102,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
100102
userJoined?: (data: any) => void
101103
userLeft?: (data: any) => void
102104
workflowDeleted?: (data: any) => void
105+
workflowReverted?: (data: any) => void
103106
}>({})
104107

105108
// Helper function to generate a fresh socket token
@@ -281,6 +284,12 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
281284
eventHandlers.current.workflowDeleted?.(data)
282285
})
283286

287+
// Workflow revert events
288+
socketInstance.on('workflow-reverted', (data) => {
289+
logger.info(`Workflow ${data.workflowId} has been reverted to deployed state`)
290+
eventHandlers.current.workflowReverted?.(data)
291+
})
292+
284293
// Cursor update events
285294
socketInstance.on('cursor-update', (data) => {
286295
setPresenceUsers((prev) =>
@@ -557,6 +566,10 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
557566
eventHandlers.current.workflowDeleted = handler
558567
}, [])
559568

569+
const onWorkflowReverted = useCallback((handler: (data: any) => void) => {
570+
eventHandlers.current.workflowReverted = handler
571+
}, [])
572+
560573
return (
561574
<SocketContext.Provider
562575
value={{
@@ -578,6 +591,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
578591
onUserJoined,
579592
onUserLeft,
580593
onWorkflowDeleted,
594+
onWorkflowReverted,
581595
}}
582596
>
583597
{children}

apps/sim/hooks/use-collaborative-workflow.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export function useCollaborativeWorkflow() {
2525
onUserJoined,
2626
onUserLeft,
2727
onWorkflowDeleted,
28+
onWorkflowReverted,
2829
} = useSocket()
2930

3031
const { activeWorkflowId } = useWorkflowRegistry()
@@ -262,12 +263,80 @@ export function useCollaborativeWorkflow() {
262263
}
263264
}
264265

266+
const handleWorkflowReverted = async (data: any) => {
267+
const { workflowId } = data
268+
logger.info(`Workflow ${workflowId} has been reverted to deployed state`)
269+
270+
// If the reverted workflow is the currently active one, reload the workflow state
271+
if (activeWorkflowId === workflowId) {
272+
logger.info(`Currently active workflow ${workflowId} was reverted, reloading state`)
273+
274+
try {
275+
// Fetch the updated workflow state from the server (which loads from normalized tables)
276+
const response = await fetch(`/api/workflows/${workflowId}`)
277+
if (response.ok) {
278+
const responseData = await response.json()
279+
const workflowData = responseData.data
280+
281+
if (workflowData?.state) {
282+
// Update the workflow store with the reverted state
283+
isApplyingRemoteChange.current = true
284+
try {
285+
// Update the main workflow state using the API response
286+
useWorkflowStore.setState({
287+
blocks: workflowData.state.blocks || {},
288+
edges: workflowData.state.edges || [],
289+
loops: workflowData.state.loops || {},
290+
parallels: workflowData.state.parallels || {},
291+
isDeployed: workflowData.state.isDeployed || false,
292+
deployedAt: workflowData.state.deployedAt,
293+
lastSaved: workflowData.state.lastSaved || Date.now(),
294+
hasActiveSchedule: workflowData.state.hasActiveSchedule || false,
295+
hasActiveWebhook: workflowData.state.hasActiveWebhook || false,
296+
deploymentStatuses: workflowData.state.deploymentStatuses || {},
297+
})
298+
299+
// Update subblock store with reverted values
300+
const subblockValues: Record<string, Record<string, any>> = {}
301+
Object.entries(workflowData.state.blocks || {}).forEach(([blockId, block]) => {
302+
const blockState = block as any
303+
subblockValues[blockId] = {}
304+
Object.entries(blockState.subBlocks || {}).forEach(([subblockId, subblock]) => {
305+
subblockValues[blockId][subblockId] = (subblock as any).value
306+
})
307+
})
308+
309+
// Update subblock store for this workflow
310+
useSubBlockStore.setState((state: any) => ({
311+
workflowValues: {
312+
...state.workflowValues,
313+
[workflowId]: subblockValues,
314+
},
315+
}))
316+
317+
logger.info(`Successfully loaded reverted workflow state for ${workflowId}`)
318+
} finally {
319+
isApplyingRemoteChange.current = false
320+
}
321+
} else {
322+
logger.error('No state found in workflow data after revert', { workflowData })
323+
}
324+
} else {
325+
logger.error(`Failed to fetch workflow data after revert: ${response.statusText}`)
326+
}
327+
} catch (error) {
328+
logger.error('Error reloading workflow state after revert:', error)
329+
}
330+
}
331+
}
332+
265333
// Register event handlers
266334
onWorkflowOperation(handleWorkflowOperation)
267335
onSubblockUpdate(handleSubblockUpdate)
268336
onUserJoined(handleUserJoined)
269337
onUserLeft(handleUserLeft)
270338
onWorkflowDeleted(handleWorkflowDeleted)
339+
onWorkflowReverted(handleWorkflowReverted)
271340

272341
return () => {
273342
// Cleanup handled by socket context
@@ -278,6 +347,7 @@ export function useCollaborativeWorkflow() {
278347
onUserJoined,
279348
onUserLeft,
280349
onWorkflowDeleted,
350+
onWorkflowReverted,
281351
workflowStore,
282352
subBlockStore,
283353
activeWorkflowId,

apps/sim/socket-server/rooms/manager.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,26 @@ export class RoomManager {
115115
)
116116
}
117117

118+
handleWorkflowRevert(workflowId: string, timestamp: number) {
119+
logger.info(`Handling workflow revert notification for ${workflowId}`)
120+
121+
const room = this.workflowRooms.get(workflowId)
122+
if (!room) {
123+
logger.debug(`No active room found for reverted workflow ${workflowId}`)
124+
return
125+
}
126+
127+
this.io.to(workflowId).emit('workflow-reverted', {
128+
workflowId,
129+
message: 'Workflow has been reverted to deployed state',
130+
timestamp,
131+
})
132+
133+
room.lastModified = timestamp
134+
135+
logger.info(`Notified ${room.users.size} users about workflow revert: ${workflowId}`)
136+
}
137+
118138
async validateWorkflowConsistency(
119139
workflowId: string
120140
): Promise<{ valid: boolean; issues: string[] }> {

apps/sim/socket-server/routes/http.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,27 @@ export function createHttpHandler(roomManager: RoomManager, logger: Logger) {
5050
return
5151
}
5252

53+
// Handle workflow revert notifications from the main API
54+
if (req.method === 'POST' && req.url === '/api/workflow-reverted') {
55+
let body = ''
56+
req.on('data', (chunk) => {
57+
body += chunk.toString()
58+
})
59+
req.on('end', () => {
60+
try {
61+
const { workflowId, timestamp } = JSON.parse(body)
62+
roomManager.handleWorkflowRevert(workflowId, timestamp)
63+
res.writeHead(200, { 'Content-Type': 'application/json' })
64+
res.end(JSON.stringify({ success: true }))
65+
} catch (error) {
66+
logger.error('Error handling workflow revert notification:', error)
67+
res.writeHead(500, { 'Content-Type': 'application/json' })
68+
res.end(JSON.stringify({ error: 'Failed to process revert notification' }))
69+
}
70+
})
71+
return
72+
}
73+
5374
res.writeHead(404, { 'Content-Type': 'application/json' })
5475
res.end(JSON.stringify({ error: 'Not found' }))
5576
}

0 commit comments

Comments
 (0)