1+ import { eq } from 'drizzle-orm'
2+ import { type NextRequest , NextResponse } from 'next/server'
3+ import { z } from 'zod'
4+ import { getSession } from '@/lib/auth'
5+ import { createLogger } from '@/lib/logs/console-logger'
6+ import { getUserEntityPermissions } from '@/lib/permissions/utils'
7+ import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
8+ import { db } from '@/db'
9+ import { workflow } from '@/db/schema'
10+
11+ const logger = createLogger ( 'WorkflowStateAPI' )
12+
13+ const WorkflowStateSchema = z . object ( {
14+ blocks : z . record ( z . any ( ) ) ,
15+ edges : z . array ( z . any ( ) ) ,
16+ loops : z . record ( z . any ( ) ) . optional ( ) ,
17+ parallels : z . record ( z . any ( ) ) . optional ( ) ,
18+ lastSaved : z . number ( ) . optional ( ) ,
19+ isDeployed : z . boolean ( ) . optional ( ) ,
20+ deployedAt : z . date ( ) . optional ( ) ,
21+ deploymentStatuses : z . record ( z . any ( ) ) . optional ( ) ,
22+ hasActiveSchedule : z . boolean ( ) . optional ( ) ,
23+ hasActiveWebhook : z . boolean ( ) . optional ( ) ,
24+ } )
25+
26+ /**
27+ * PUT /api/workflows/[id]/state
28+ * Save complete workflow state to normalized database tables
29+ */
30+ export async function PUT ( request : NextRequest , { params } : { params : Promise < { id : string } > } ) {
31+ const requestId = crypto . randomUUID ( ) . slice ( 0 , 8 )
32+ const startTime = Date . now ( )
33+ const { id : workflowId } = await params
34+
35+ try {
36+ // Get the session
37+ const session = await getSession ( )
38+ if ( ! session ?. user ?. id ) {
39+ logger . warn ( `[${ requestId } ] Unauthorized state update attempt for workflow ${ workflowId } ` )
40+ return NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } )
41+ }
42+
43+ const userId = session . user . id
44+
45+ // Parse and validate request body
46+ const body = await request . json ( )
47+ const state = WorkflowStateSchema . parse ( body )
48+
49+ // Fetch the workflow to check ownership/access
50+ const workflowData = await db
51+ . select ( )
52+ . from ( workflow )
53+ . where ( eq ( workflow . id , workflowId ) )
54+ . then ( ( rows ) => rows [ 0 ] )
55+
56+ if ( ! workflowData ) {
57+ logger . warn ( `[${ requestId } ] Workflow ${ workflowId } not found for state update` )
58+ return NextResponse . json ( { error : 'Workflow not found' } , { status : 404 } )
59+ }
60+
61+ // Check if user has permission to update this workflow
62+ let canUpdate = false
63+
64+ // Case 1: User owns the workflow
65+ if ( workflowData . userId === userId ) {
66+ canUpdate = true
67+ }
68+
69+ // Case 2: Workflow belongs to a workspace and user has write or admin permission
70+ if ( ! canUpdate && workflowData . workspaceId ) {
71+ const userPermission = await getUserEntityPermissions (
72+ userId ,
73+ 'workspace' ,
74+ workflowData . workspaceId
75+ )
76+ if ( userPermission === 'write' || userPermission === 'admin' ) {
77+ canUpdate = true
78+ }
79+ }
80+
81+ if ( ! canUpdate ) {
82+ logger . warn (
83+ `[${ requestId } ] User ${ userId } denied permission to update workflow state ${ workflowId } `
84+ )
85+ return NextResponse . json ( { error : 'Access denied' } , { status : 403 } )
86+ }
87+
88+ // Save to normalized tables
89+ logger . info ( `[${ requestId } ] Saving workflow ${ workflowId } state to normalized tables` )
90+
91+ // Ensure all required fields are present for WorkflowState type
92+ const workflowState = {
93+ blocks : state . blocks ,
94+ edges : state . edges ,
95+ loops : state . loops || { } ,
96+ parallels : state . parallels || { } ,
97+ lastSaved : state . lastSaved || Date . now ( ) ,
98+ isDeployed : state . isDeployed || false ,
99+ deployedAt : state . deployedAt ,
100+ deploymentStatuses : state . deploymentStatuses || { } ,
101+ hasActiveSchedule : state . hasActiveSchedule || false ,
102+ hasActiveWebhook : state . hasActiveWebhook || false ,
103+ }
104+
105+ const saveResult = await saveWorkflowToNormalizedTables ( workflowId , workflowState )
106+
107+ if ( ! saveResult . success ) {
108+ logger . error ( `[${ requestId } ] Failed to save workflow ${ workflowId } state:` , saveResult . error )
109+ return NextResponse . json (
110+ { error : 'Failed to save workflow state' , details : saveResult . error } ,
111+ { status : 500 }
112+ )
113+ }
114+
115+ // Update workflow's lastSynced timestamp
116+ await db
117+ . update ( workflow )
118+ . set ( {
119+ lastSynced : new Date ( ) ,
120+ updatedAt : new Date ( ) ,
121+ state : saveResult . jsonBlob // Also update JSON blob for backward compatibility
122+ } )
123+ . where ( eq ( workflow . id , workflowId ) )
124+
125+ const elapsed = Date . now ( ) - startTime
126+ logger . info ( `[${ requestId } ] Successfully saved workflow ${ workflowId } state in ${ elapsed } ms` )
127+
128+ return NextResponse . json ( {
129+ success : true ,
130+ blocksCount : Object . keys ( state . blocks ) . length ,
131+ edgesCount : state . edges . length
132+ } , { status : 200 } )
133+
134+ } catch ( error : any ) {
135+ const elapsed = Date . now ( ) - startTime
136+ if ( error instanceof z . ZodError ) {
137+ logger . warn ( `[${ requestId } ] Invalid workflow state data for ${ workflowId } ` , {
138+ errors : error . errors ,
139+ } )
140+ return NextResponse . json (
141+ { error : 'Invalid state data' , details : error . errors } ,
142+ { status : 400 }
143+ )
144+ }
145+
146+ logger . error ( `[${ requestId } ] Error saving workflow ${ workflowId } state after ${ elapsed } ms` , error )
147+ return NextResponse . json ( { error : 'Internal server error' } , { status : 500 } )
148+ }
149+ }
0 commit comments