Skip to content

Commit 78e4ca9

Browse files
improvement(serializer): canonical subblock, serialization cleanups, schedules/webhooks are deployment version friendly (#2848)
* hide form deployment tab from docs * progress * fix resolution * cleanup code * fix positioning * cleanup dead sockets adv mode ops * address greptile comments * fix tests plus more simplification * fix cleanup * bring back advanced mode with specific definition * revert feature flags * improvement(subblock): ui * resolver change to make all var references optional chaining * fix(webhooks/schedules): deployment version friendly * fix tests * fix credential sets with new lifecycle * prep merge * add back migration * fix display check for adv fields * fix trigger vs block scoping --------- Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
1 parent ce3ddb6 commit 78e4ca9

File tree

70 files changed

+12802
-1007
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+12802
-1007
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"pages": ["index", "basics", "api", "form", "logging", "costs"]
2+
"pages": ["index", "basics", "api", "logging", "costs"]
33
}

apps/sim/app/api/copilot/execute-tool/route.ts

Lines changed: 12 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ import {
1414
import { generateRequestId } from '@/lib/core/utils/request'
1515
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
1616
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
17-
import { REFERENCE } from '@/executor/constants'
18-
import { createEnvVarPattern } from '@/executor/utils/reference-validation'
17+
import { resolveEnvVarReferences } from '@/executor/utils/reference-validation'
1918
import { executeTool } from '@/tools'
2019
import { getTool, resolveToolId } from '@/tools/utils'
2120

@@ -28,45 +27,6 @@ const ExecuteToolSchema = z.object({
2827
workflowId: z.string().optional(),
2928
})
3029

31-
/**
32-
* Resolves all {{ENV_VAR}} references in a value recursively
33-
* Works with strings, arrays, and objects
34-
*/
35-
function resolveEnvVarReferences(value: any, envVars: Record<string, string>): any {
36-
if (typeof value === 'string') {
37-
// Check for exact match: entire string is "{{VAR_NAME}}"
38-
const exactMatchPattern = new RegExp(
39-
`^\\${REFERENCE.ENV_VAR_START}([^}]+)\\${REFERENCE.ENV_VAR_END}$`
40-
)
41-
const exactMatch = exactMatchPattern.exec(value)
42-
if (exactMatch) {
43-
const envVarName = exactMatch[1].trim()
44-
return envVars[envVarName] ?? value
45-
}
46-
47-
// Check for embedded references: "prefix {{VAR}} suffix"
48-
const envVarPattern = createEnvVarPattern()
49-
return value.replace(envVarPattern, (match, varName) => {
50-
const trimmedName = varName.trim()
51-
return envVars[trimmedName] ?? match
52-
})
53-
}
54-
55-
if (Array.isArray(value)) {
56-
return value.map((item) => resolveEnvVarReferences(item, envVars))
57-
}
58-
59-
if (value !== null && typeof value === 'object') {
60-
const resolved: Record<string, any> = {}
61-
for (const [key, val] of Object.entries(value)) {
62-
resolved[key] = resolveEnvVarReferences(val, envVars)
63-
}
64-
return resolved
65-
}
66-
67-
return value
68-
}
69-
7030
export async function POST(req: NextRequest) {
7131
const tracker = createRequestTracker()
7232

@@ -145,7 +105,17 @@ export async function POST(req: NextRequest) {
145105

146106
// Build execution params starting with LLM-provided arguments
147107
// Resolve all {{ENV_VAR}} references in the arguments
148-
const executionParams: Record<string, any> = resolveEnvVarReferences(toolArgs, decryptedEnvVars)
108+
const executionParams: Record<string, any> = resolveEnvVarReferences(
109+
toolArgs,
110+
decryptedEnvVars,
111+
{
112+
resolveExactMatch: true,
113+
allowEmbedded: true,
114+
trimKeys: true,
115+
onMissing: 'keep',
116+
deep: true,
117+
}
118+
) as Record<string, any>
149119

150120
logger.info(`[${tracker.requestId}] Resolved env var references in arguments`, {
151121
toolName,

apps/sim/app/api/function/execute/route.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
99
import {
1010
createEnvVarPattern,
1111
createWorkflowVariablePattern,
12+
resolveEnvVarReferences,
1213
} from '@/executor/utils/reference-validation'
1314
export const dynamic = 'force-dynamic'
1415
export const runtime = 'nodejs'
@@ -479,9 +480,29 @@ function resolveEnvironmentVariables(
479480
const replacements: Array<{ match: string; index: number; varName: string; varValue: string }> =
480481
[]
481482

483+
const resolverVars: Record<string, string> = {}
484+
Object.entries(params).forEach(([key, value]) => {
485+
if (value) {
486+
resolverVars[key] = String(value)
487+
}
488+
})
489+
Object.entries(envVars).forEach(([key, value]) => {
490+
if (value) {
491+
resolverVars[key] = value
492+
}
493+
})
494+
482495
while ((match = regex.exec(code)) !== null) {
483496
const varName = match[1].trim()
484-
const varValue = envVars[varName] || params[varName] || ''
497+
const resolved = resolveEnvVarReferences(match[0], resolverVars, {
498+
allowEmbedded: true,
499+
resolveExactMatch: true,
500+
trimKeys: true,
501+
onMissing: 'empty',
502+
deep: false,
503+
})
504+
const varValue =
505+
typeof resolved === 'string' ? resolved : resolved == null ? '' : String(resolved)
485506
replacements.push({
486507
match: match[0],
487508
index: match.index,

apps/sim/app/api/mcp/servers/test-connection/route.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import { McpClient } from '@/lib/mcp/client'
55
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
66
import type { McpServerConfig, McpTransport } from '@/lib/mcp/types'
77
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
8-
import { REFERENCE } from '@/executor/constants'
9-
import { createEnvVarPattern } from '@/executor/utils/reference-validation'
8+
import { resolveEnvVarReferences } from '@/executor/utils/reference-validation'
109

1110
const logger = createLogger('McpServerTestAPI')
1211

@@ -24,22 +23,23 @@ function isUrlBasedTransport(transport: McpTransport): boolean {
2423
* Resolve environment variables in strings
2524
*/
2625
function resolveEnvVars(value: string, envVars: Record<string, string>): string {
27-
const envVarPattern = createEnvVarPattern()
28-
const envMatches = value.match(envVarPattern)
29-
if (!envMatches) return value
30-
31-
let resolvedValue = value
32-
for (const match of envMatches) {
33-
const envKey = match.slice(REFERENCE.ENV_VAR_START.length, -REFERENCE.ENV_VAR_END.length).trim()
34-
const envValue = envVars[envKey]
35-
36-
if (envValue === undefined) {
26+
const missingVars: string[] = []
27+
const resolvedValue = resolveEnvVarReferences(value, envVars, {
28+
allowEmbedded: true,
29+
resolveExactMatch: true,
30+
trimKeys: true,
31+
onMissing: 'keep',
32+
deep: false,
33+
missingKeys: missingVars,
34+
}) as string
35+
36+
if (missingVars.length > 0) {
37+
const uniqueMissing = Array.from(new Set(missingVars))
38+
uniqueMissing.forEach((envKey) => {
3739
logger.warn(`Environment variable "${envKey}" not found in MCP server test`)
38-
continue
39-
}
40-
41-
resolvedValue = resolvedValue.replace(match, envValue)
40+
})
4241
}
42+
4343
return resolvedValue
4444
}
4545

apps/sim/app/api/schedules/execute/route.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe('Scheduled Workflow Execution API Route', () => {
5757
not: vi.fn((condition) => ({ type: 'not', condition })),
5858
isNull: vi.fn((field) => ({ type: 'isNull', field })),
5959
or: vi.fn((...conditions) => ({ type: 'or', conditions })),
60+
sql: vi.fn((strings, ...values) => ({ type: 'sql', strings, values })),
6061
}))
6162

6263
vi.doMock('@sim/db', () => {
@@ -92,6 +93,17 @@ describe('Scheduled Workflow Execution API Route', () => {
9293
status: 'status',
9394
nextRunAt: 'nextRunAt',
9495
lastQueuedAt: 'lastQueuedAt',
96+
deploymentVersionId: 'deploymentVersionId',
97+
},
98+
workflowDeploymentVersion: {
99+
id: 'id',
100+
workflowId: 'workflowId',
101+
isActive: 'isActive',
102+
},
103+
workflow: {
104+
id: 'id',
105+
userId: 'userId',
106+
workspaceId: 'workspaceId',
95107
},
96108
}
97109
})
@@ -134,6 +146,7 @@ describe('Scheduled Workflow Execution API Route', () => {
134146
not: vi.fn((condition) => ({ type: 'not', condition })),
135147
isNull: vi.fn((field) => ({ type: 'isNull', field })),
136148
or: vi.fn((...conditions) => ({ type: 'or', conditions })),
149+
sql: vi.fn((strings, ...values) => ({ type: 'sql', strings, values })),
137150
}))
138151

139152
vi.doMock('@sim/db', () => {
@@ -169,6 +182,17 @@ describe('Scheduled Workflow Execution API Route', () => {
169182
status: 'status',
170183
nextRunAt: 'nextRunAt',
171184
lastQueuedAt: 'lastQueuedAt',
185+
deploymentVersionId: 'deploymentVersionId',
186+
},
187+
workflowDeploymentVersion: {
188+
id: 'id',
189+
workflowId: 'workflowId',
190+
isActive: 'isActive',
191+
},
192+
workflow: {
193+
id: 'id',
194+
userId: 'userId',
195+
workspaceId: 'workspaceId',
172196
},
173197
}
174198
})
@@ -206,6 +230,7 @@ describe('Scheduled Workflow Execution API Route', () => {
206230
not: vi.fn((condition) => ({ type: 'not', condition })),
207231
isNull: vi.fn((field) => ({ type: 'isNull', field })),
208232
or: vi.fn((...conditions) => ({ type: 'or', conditions })),
233+
sql: vi.fn((strings, ...values) => ({ type: 'sql', strings, values })),
209234
}))
210235

211236
vi.doMock('@sim/db', () => {
@@ -228,6 +253,17 @@ describe('Scheduled Workflow Execution API Route', () => {
228253
status: 'status',
229254
nextRunAt: 'nextRunAt',
230255
lastQueuedAt: 'lastQueuedAt',
256+
deploymentVersionId: 'deploymentVersionId',
257+
},
258+
workflowDeploymentVersion: {
259+
id: 'id',
260+
workflowId: 'workflowId',
261+
isActive: 'isActive',
262+
},
263+
workflow: {
264+
id: 'id',
265+
userId: 'userId',
266+
workspaceId: 'workspaceId',
231267
},
232268
}
233269
})
@@ -265,6 +301,7 @@ describe('Scheduled Workflow Execution API Route', () => {
265301
not: vi.fn((condition) => ({ type: 'not', condition })),
266302
isNull: vi.fn((field) => ({ type: 'isNull', field })),
267303
or: vi.fn((...conditions) => ({ type: 'or', conditions })),
304+
sql: vi.fn((strings, ...values) => ({ type: 'sql', strings, values })),
268305
}))
269306

270307
vi.doMock('@sim/db', () => {
@@ -310,6 +347,17 @@ describe('Scheduled Workflow Execution API Route', () => {
310347
status: 'status',
311348
nextRunAt: 'nextRunAt',
312349
lastQueuedAt: 'lastQueuedAt',
350+
deploymentVersionId: 'deploymentVersionId',
351+
},
352+
workflowDeploymentVersion: {
353+
id: 'id',
354+
workflowId: 'workflowId',
355+
isActive: 'isActive',
356+
},
357+
workflow: {
358+
id: 'id',
359+
userId: 'userId',
360+
workspaceId: 'workspaceId',
313361
},
314362
}
315363
})

apps/sim/app/api/schedules/execute/route.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { db, workflowSchedule } from '@sim/db'
1+
import { db, workflowDeploymentVersion, workflowSchedule } from '@sim/db'
22
import { createLogger } from '@sim/logger'
33
import { tasks } from '@trigger.dev/sdk'
4-
import { and, eq, isNull, lt, lte, not, or } from 'drizzle-orm'
4+
import { and, eq, isNull, lt, lte, not, or, sql } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { verifyCronAuth } from '@/lib/auth/internal'
77
import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags'
@@ -37,7 +37,8 @@ export async function GET(request: NextRequest) {
3737
or(
3838
isNull(workflowSchedule.lastQueuedAt),
3939
lt(workflowSchedule.lastQueuedAt, workflowSchedule.nextRunAt)
40-
)
40+
),
41+
sql`${workflowSchedule.deploymentVersionId} = (select ${workflowDeploymentVersion.id} from ${workflowDeploymentVersion} where ${workflowDeploymentVersion.workflowId} = ${workflowSchedule.workflowId} and ${workflowDeploymentVersion.isActive} = true)`
4142
)
4243
)
4344
.returning({

apps/sim/app/api/schedules/route.test.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,23 @@ vi.mock('@sim/db', () => ({
2929

3030
vi.mock('@sim/db/schema', () => ({
3131
workflow: { id: 'id', userId: 'userId', workspaceId: 'workspaceId' },
32-
workflowSchedule: { workflowId: 'workflowId', blockId: 'blockId' },
32+
workflowSchedule: {
33+
workflowId: 'workflowId',
34+
blockId: 'blockId',
35+
deploymentVersionId: 'deploymentVersionId',
36+
},
37+
workflowDeploymentVersion: {
38+
id: 'id',
39+
workflowId: 'workflowId',
40+
isActive: 'isActive',
41+
},
3342
}))
3443

3544
vi.mock('drizzle-orm', () => ({
3645
eq: vi.fn(),
3746
and: vi.fn(),
47+
or: vi.fn(),
48+
isNull: vi.fn(),
3849
}))
3950

4051
vi.mock('@/lib/core/utils/request', () => ({
@@ -56,6 +67,11 @@ function mockDbChain(results: any[]) {
5667
where: () => ({
5768
limit: () => results[callIndex++] || [],
5869
}),
70+
leftJoin: () => ({
71+
where: () => ({
72+
limit: () => results[callIndex++] || [],
73+
}),
74+
}),
5975
}),
6076
}))
6177
}
@@ -74,7 +90,16 @@ describe('Schedule GET API', () => {
7490
it('returns schedule data for authorized user', async () => {
7591
mockDbChain([
7692
[{ userId: 'user-1', workspaceId: null }],
77-
[{ id: 'sched-1', cronExpression: '0 9 * * *', status: 'active', failedCount: 0 }],
93+
[
94+
{
95+
schedule: {
96+
id: 'sched-1',
97+
cronExpression: '0 9 * * *',
98+
status: 'active',
99+
failedCount: 0,
100+
},
101+
},
102+
],
78103
])
79104

80105
const res = await GET(createRequest('http://test/api/schedules?workflowId=wf-1'))
@@ -128,7 +153,7 @@ describe('Schedule GET API', () => {
128153
it('allows workspace members to view', async () => {
129154
mockDbChain([
130155
[{ userId: 'other-user', workspaceId: 'ws-1' }],
131-
[{ id: 'sched-1', status: 'active', failedCount: 0 }],
156+
[{ schedule: { id: 'sched-1', status: 'active', failedCount: 0 } }],
132157
])
133158

134159
const res = await GET(createRequest('http://test/api/schedules?workflowId=wf-1'))
@@ -139,7 +164,7 @@ describe('Schedule GET API', () => {
139164
it('indicates disabled schedule with failures', async () => {
140165
mockDbChain([
141166
[{ userId: 'user-1', workspaceId: null }],
142-
[{ id: 'sched-1', status: 'disabled', failedCount: 100 }],
167+
[{ schedule: { id: 'sched-1', status: 'disabled', failedCount: 100 } }],
143168
])
144169

145170
const res = await GET(createRequest('http://test/api/schedules?workflowId=wf-1'))

0 commit comments

Comments
 (0)