Skip to content

Commit 53b2a06

Browse files
committed
more fixes
1 parent 2e775e9 commit 53b2a06

17 files changed

Lines changed: 814 additions & 343 deletions

File tree

apps/sim/app/api/workflows/[id]/route.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
FolderLockedError,
99
WorkflowLockedError,
1010
} from '@sim/workflow-authz'
11-
import { and, eq, isNull, ne } from 'drizzle-orm'
11+
import { and, eq, isNull, ne, sql } from 'drizzle-orm'
1212
import { type NextRequest, NextResponse } from 'next/server'
1313
import { updateWorkflowContract } from '@/lib/api/contracts/workflows'
1414
import { parseRequest } from '@/lib/api/server'
@@ -85,36 +85,44 @@ export const GET = withRouteHandler(
8585
}
8686
}
8787

88-
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
88+
const snapshot = await db.transaction(async (tx) => {
89+
await tx.execute(sql`SET TRANSACTION ISOLATION LEVEL REPEATABLE READ`)
90+
const [normalizedData, [workflowRecord]] = await Promise.all([
91+
loadWorkflowFromNormalizedTables(workflowId, tx),
92+
tx.select().from(workflow).where(eq(workflow.id, workflowId)).limit(1),
93+
])
94+
return { normalizedData, workflowRecord }
95+
})
96+
const responseWorkflowData = snapshot.workflowRecord ?? workflowData
8997

9098
// Stamp `workflowId` from the path param on each variable so the
9199
// global client-side variables store can filter by workflow without
92100
// requiring persisted variables to carry a redundant `workflowId`.
93101
// The persisted blob may or may not include `workflowId` depending on
94102
// when the variable was last written; the path param is authoritative.
95103
const persistedVariables =
96-
(workflowData.variables as Record<string, Record<string, unknown>>) || {}
104+
(responseWorkflowData.variables as Record<string, Record<string, unknown>>) || {}
97105
const stampedVariables: Record<string, Record<string, unknown>> = {}
98106
for (const [variableId, variable] of Object.entries(persistedVariables)) {
99107
if (variable && typeof variable === 'object') {
100108
stampedVariables[variableId] = { ...variable, workflowId }
101109
}
102110
}
103111

104-
if (normalizedData) {
112+
if (snapshot.normalizedData) {
105113
const finalWorkflowData = {
106-
...workflowData,
114+
...responseWorkflowData,
107115
state: {
108-
blocks: normalizedData.blocks,
109-
edges: normalizedData.edges,
110-
loops: normalizedData.loops,
111-
parallels: normalizedData.parallels,
116+
blocks: snapshot.normalizedData.blocks,
117+
edges: snapshot.normalizedData.edges,
118+
loops: snapshot.normalizedData.loops,
119+
parallels: snapshot.normalizedData.parallels,
112120
lastSaved: Date.now(),
113-
isDeployed: workflowData.isDeployed || false,
114-
deployedAt: workflowData.deployedAt,
121+
isDeployed: responseWorkflowData.isDeployed || false,
122+
deployedAt: responseWorkflowData.deployedAt,
115123
metadata: {
116-
name: workflowData.name,
117-
description: workflowData.description,
124+
name: responseWorkflowData.name,
125+
description: responseWorkflowData.description,
118126
},
119127
},
120128
variables: stampedVariables,
@@ -128,18 +136,18 @@ export const GET = withRouteHandler(
128136
}
129137

130138
const emptyWorkflowData = {
131-
...workflowData,
139+
...responseWorkflowData,
132140
state: {
133141
blocks: {},
134142
edges: [],
135143
loops: {},
136144
parallels: {},
137145
lastSaved: Date.now(),
138-
isDeployed: workflowData.isDeployed || false,
139-
deployedAt: workflowData.deployedAt,
146+
isDeployed: responseWorkflowData.isDeployed || false,
147+
deployedAt: responseWorkflowData.deployedAt,
140148
metadata: {
141-
name: workflowData.name,
142-
description: workflowData.description,
149+
name: responseWorkflowData.name,
150+
description: responseWorkflowData.description,
143151
},
144152
},
145153
variables: stampedVariables,

apps/sim/app/api/workflows/[id]/state/route.ts

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
authorizeWorkflowByWorkspacePermission,
88
WorkflowLockedError,
99
} from '@sim/workflow-authz'
10-
import { eq } from 'drizzle-orm'
10+
import { eq, sql } from 'drizzle-orm'
1111
import { type NextRequest, NextResponse } from 'next/server'
1212
import { putWorkflowNormalizedStateContract } from '@/lib/api/contracts/workflows'
1313
import { parseRequest } from '@/lib/api/server'
@@ -52,8 +52,20 @@ export const GET = withRouteHandler(
5252
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
5353
}
5454

55-
const normalized = await loadWorkflowFromNormalizedTables(workflowId)
56-
if (!normalized) {
55+
const snapshot = await db.transaction(async (tx) => {
56+
await tx.execute(sql`SET TRANSACTION ISOLATION LEVEL REPEATABLE READ`)
57+
const [normalized, [workflowRecord]] = await Promise.all([
58+
loadWorkflowFromNormalizedTables(workflowId, tx),
59+
tx
60+
.select({ variables: workflow.variables })
61+
.from(workflow)
62+
.where(eq(workflow.id, workflowId))
63+
.limit(1),
64+
])
65+
return { normalized, variables: workflowRecord?.variables }
66+
})
67+
68+
if (!snapshot.normalized) {
5769
return NextResponse.json({ error: 'Workflow state not found' }, { status: 404 })
5870
}
5971

@@ -62,7 +74,7 @@ export const GET = withRouteHandler(
6274
// requiring clients to thread the path param through. The read
6375
// contract requires this server-stamped field.
6476
const persistedVariables =
65-
(authorization.workflow?.variables as Record<string, Record<string, unknown>>) || {}
77+
(snapshot.variables as Record<string, Record<string, unknown>>) || {}
6678
const variables: Record<string, Record<string, unknown>> = {}
6779
for (const [variableId, variable] of Object.entries(persistedVariables)) {
6880
if (variable && typeof variable === 'object') {
@@ -71,10 +83,10 @@ export const GET = withRouteHandler(
7183
}
7284

7385
return NextResponse.json({
74-
blocks: normalized.blocks,
75-
edges: normalized.edges,
76-
loops: normalized.loops || {},
77-
parallels: normalized.parallels || {},
86+
blocks: snapshot.normalized.blocks,
87+
edges: snapshot.normalized.edges,
88+
loops: snapshot.normalized.loops || {},
89+
parallels: snapshot.normalized.parallels || {},
7890
variables,
7991
})
8092
} catch (error) {
@@ -185,10 +197,41 @@ export const PUT = withRouteHandler(
185197
deployedAt: state.deployedAt,
186198
}
187199

188-
const saveResult = await saveWorkflowToNormalizedTables(
189-
workflowId,
190-
workflowState as WorkflowState
191-
)
200+
const saveResult = await db.transaction(async (tx) => {
201+
await tx
202+
.select({ id: workflow.id })
203+
.from(workflow)
204+
.where(eq(workflow.id, workflowId))
205+
.limit(1)
206+
.for('update')
207+
208+
const result = await saveWorkflowToNormalizedTables(
209+
workflowId,
210+
workflowState as WorkflowState,
211+
tx
212+
)
213+
214+
if (!result.success) return result
215+
216+
// Update workflow's lastSynced timestamp and variables if provided
217+
const updateData: {
218+
lastSynced: Date
219+
updatedAt: Date
220+
variables?: typeof state.variables
221+
} = {
222+
lastSynced: new Date(),
223+
updatedAt: new Date(),
224+
}
225+
226+
// If variables are provided in the state, update them in the workflow record
227+
if (state.variables !== undefined) {
228+
updateData.variables = state.variables
229+
}
230+
231+
await tx.update(workflow).set(updateData).where(eq(workflow.id, workflowId))
232+
233+
return result
234+
})
192235

193236
if (!saveResult.success) {
194237
logger.error(
@@ -235,19 +278,6 @@ export const PUT = withRouteHandler(
235278
logger.error(`[${requestId}] Failed to persist custom tools`, { error, workflowId })
236279
}
237280

238-
// Update workflow's lastSynced timestamp and variables if provided
239-
const updateData: any = {
240-
lastSynced: new Date(),
241-
updatedAt: new Date(),
242-
}
243-
244-
// If variables are provided in the state, update them in the workflow record
245-
if (state.variables !== undefined) {
246-
updateData.variables = state.variables
247-
}
248-
249-
await db.update(workflow).set(updateData).where(eq(workflow.id, workflowId))
250-
251281
const elapsed = Date.now() - startTime
252282
logger.info(`[${requestId}] Successfully saved workflow ${workflowId} state in ${elapsed}ms`)
253283

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/versions.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ interface VersionsProps {
3232
workflowId: string | null
3333
versions: WorkflowDeploymentVersionResponse[]
3434
versionsLoading: boolean
35+
isPromotingVersion: boolean
3536
selectedVersion: number | null
3637
onSelectVersion: (version: number | null) => void
3738
onPromoteToLive: (version: number) => void
@@ -46,6 +47,7 @@ export function Versions({
4647
workflowId,
4748
versions,
4849
versionsLoading,
50+
isPromotingVersion,
4951
selectedVersion,
5052
onSelectVersion,
5153
onPromoteToLive,
@@ -115,6 +117,7 @@ export function Versions({
115117
}
116118

117119
const handlePromote = (version: number) => {
120+
if (isPromotingVersion) return
118121
setOpenDropdown(null)
119122
onPromoteToLive(version)
120123
}
@@ -320,7 +323,7 @@ export function Versions({
320323
onOpenChange={(open) => setOpenDropdown(open ? v.version : null)}
321324
>
322325
<PopoverTrigger asChild>
323-
<Button variant='ghost' className='!p-1'>
326+
<Button variant='ghost' className='!p-1' disabled={isPromotingVersion}>
324327
<MoreVertical className='h-3.5 w-3.5' />
325328
</Button>
326329
</PopoverTrigger>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
Tooltip,
1818
} from '@/components/emcn'
1919
import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils'
20+
import type { DeployReadiness } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deploy-readiness'
2021
import { Preview, PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview'
2122
import { useDeploymentVersionState, useRevertToVersion } from '@/hooks/queries/workflows'
2223
import type { WorkflowState } from '@/stores/workflows/workflow/types'
@@ -30,8 +31,11 @@ interface GeneralDeployProps {
3031
isLoadingDeployedState: boolean
3132
versions: WorkflowDeploymentVersionResponse[]
3233
versionsLoading: boolean
34+
isPromotingVersion: boolean
35+
deployReadiness: DeployReadiness
3336
onPromoteToLive: (version: number) => Promise<void>
3437
onLoadDeploymentComplete: () => void
38+
onLoadDeploymentBlocked: (message: string) => void
3539
}
3640

3741
type PreviewMode = 'active' | 'selected'
@@ -45,8 +49,11 @@ export function GeneralDeploy({
4549
isLoadingDeployedState,
4650
versions,
4751
versionsLoading,
52+
isPromotingVersion,
53+
deployReadiness,
4854
onPromoteToLive,
4955
onLoadDeploymentComplete,
56+
onLoadDeploymentBlocked,
5057
}: GeneralDeployProps) {
5158
const [selectedVersion, setSelectedVersion] = useState<number | null>(null)
5259
const [showActiveDespiteSelection, setShowActiveDespiteSelection] = useState(false)
@@ -84,6 +91,10 @@ export function GeneralDeploy({
8491

8592
const confirmLoadDeployment = async () => {
8693
if (!workflowId || versionToLoad === null) return
94+
if (!(await deployReadiness.waitUntilReady())) {
95+
onLoadDeploymentBlocked(deployReadiness.tooltip)
96+
return
97+
}
8798

8899
setShowLoadDialog(false)
89100
const version = versionToLoad
@@ -98,7 +109,7 @@ export function GeneralDeploy({
98109
}
99110

100111
const confirmPromoteToLive = async () => {
101-
if (versionToPromote === null) return
112+
if (versionToPromote === null || isPromotingVersion) return
102113

103114
setShowPromoteDialog(false)
104115
const version = versionToPromote
@@ -221,6 +232,7 @@ export function GeneralDeploy({
221232
workflowId={workflowId}
222233
versions={versions}
223234
versionsLoading={versionsLoading}
235+
isPromotingVersion={isPromotingVersion}
224236
selectedVersion={selectedVersion}
225237
onSelectVersion={handleSelectVersion}
226238
onPromoteToLive={handlePromoteToLive}
@@ -274,7 +286,7 @@ export function GeneralDeploy({
274286
<Button variant='default' onClick={() => setShowPromoteDialog(false)}>
275287
Cancel
276288
</Button>
277-
<Button variant='tertiary' onClick={confirmPromoteToLive}>
289+
<Button variant='tertiary' onClick={confirmPromoteToLive} disabled={isPromotingVersion}>
278290
Promote to live
279291
</Button>
280292
</ModalFooter>

0 commit comments

Comments
 (0)