Skip to content

Commit 4b4060f

Browse files
authored
improvement(variables): support dot notation for nested objects (#1992)
1 parent 72a048f commit 4b4060f

File tree

5 files changed

+105
-81
lines changed

5 files changed

+105
-81
lines changed

apps/sim/executor/variables/resolvers/block.ts

Lines changed: 6 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { isReference, parseReferencePath, SPECIAL_REFERENCE_PREFIXES } from '@/executor/consts'
2-
import type { ResolutionContext, Resolver } from '@/executor/variables/resolvers/reference'
2+
import {
3+
navigatePath,
4+
type ResolutionContext,
5+
type Resolver,
6+
} from '@/executor/variables/resolvers/reference'
37
import type { SerializedWorkflow } from '@/serializer/types'
48
import { normalizeBlockName } from '@/stores/workflows/utils'
59

@@ -50,7 +54,7 @@ export class BlockResolver implements Resolver {
5054
return output
5155
}
5256

53-
const result = this.navigatePath(output, pathParts)
57+
const result = navigatePath(output, pathParts)
5458

5559
if (result === undefined) {
5660
const availableKeys = output && typeof output === 'object' ? Object.keys(output) : []
@@ -83,67 +87,6 @@ export class BlockResolver implements Resolver {
8387
return this.blockByNormalizedName.get(normalized)
8488
}
8589

86-
private navigatePath(obj: any, path: string[]): any {
87-
let current = obj
88-
for (const part of path) {
89-
if (current === null || current === undefined) {
90-
return undefined
91-
}
92-
93-
const arrayMatch = part.match(/^([^[]+)\[(\d+)\](.*)$/)
94-
if (arrayMatch) {
95-
current = this.resolvePartWithIndices(current, part, '', 'block')
96-
} else if (/^\d+$/.test(part)) {
97-
const index = Number.parseInt(part, 10)
98-
current = Array.isArray(current) ? current[index] : undefined
99-
} else {
100-
current = current[part]
101-
}
102-
}
103-
return current
104-
}
105-
106-
private resolvePartWithIndices(
107-
base: any,
108-
part: string,
109-
fullPath: string,
110-
sourceName: string
111-
): any {
112-
let value = base
113-
114-
const propMatch = part.match(/^([^[]+)/)
115-
let rest = part
116-
if (propMatch) {
117-
const prop = propMatch[1]
118-
value = value[prop]
119-
rest = part.slice(prop.length)
120-
if (value === undefined) {
121-
throw new Error(`No value found at path "${fullPath}" in block "${sourceName}".`)
122-
}
123-
}
124-
125-
const indexRe = /^\[(\d+)\]/
126-
while (rest.length > 0) {
127-
const m = rest.match(indexRe)
128-
if (!m) {
129-
throw new Error(`Invalid path "${part}" in "${fullPath}" for block "${sourceName}".`)
130-
}
131-
const idx = Number.parseInt(m[1], 10)
132-
if (!Array.isArray(value)) {
133-
throw new Error(`Invalid path "${part}" in "${fullPath}" for block "${sourceName}".`)
134-
}
135-
if (idx < 0 || idx >= value.length) {
136-
throw new Error(
137-
`Array index ${idx} out of bounds (length: ${value.length}) in path "${part}"`
138-
)
139-
}
140-
value = value[idx]
141-
rest = rest.slice(m[0].length)
142-
}
143-
144-
return value
145-
}
146-
14790
public formatValueForBlock(
14891
value: any,
14992
blockType: string | undefined,

apps/sim/executor/variables/resolvers/loop.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { createLogger } from '@/lib/logs/console/logger'
22
import { isReference, parseReferencePath, REFERENCE } from '@/executor/consts'
33
import { extractBaseBlockId } from '@/executor/utils/subflow-utils'
4-
import type { ResolutionContext, Resolver } from '@/executor/variables/resolvers/reference'
4+
import {
5+
navigatePath,
6+
type ResolutionContext,
7+
type Resolver,
8+
} from '@/executor/variables/resolvers/reference'
59
import type { SerializedWorkflow } from '@/serializer/types'
610

711
const logger = createLogger('LoopResolver')
@@ -28,7 +32,7 @@ export class LoopResolver implements Resolver {
2832
return undefined
2933
}
3034

31-
const [_, property] = parts
35+
const [_, property, ...pathParts] = parts
3236
let loopScope = context.loopScope
3337

3438
if (!loopScope) {
@@ -43,19 +47,31 @@ export class LoopResolver implements Resolver {
4347
logger.warn('Loop scope not found', { reference })
4448
return undefined
4549
}
50+
51+
let value: any
4652
switch (property) {
4753
case 'iteration':
4854
case 'index':
49-
return loopScope.iteration
55+
value = loopScope.iteration
56+
break
5057
case 'item':
5158
case 'currentItem':
52-
return loopScope.item
59+
value = loopScope.item
60+
break
5361
case 'items':
54-
return loopScope.items
62+
value = loopScope.items
63+
break
5564
default:
5665
logger.warn('Unknown loop property', { property })
5766
return undefined
5867
}
68+
69+
// If there are additional path parts, navigate deeper
70+
if (pathParts.length > 0) {
71+
return navigatePath(value, pathParts)
72+
}
73+
74+
return value
5975
}
6076

6177
private findLoopForBlock(blockId: string): string | undefined {

apps/sim/executor/variables/resolvers/parallel.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { createLogger } from '@/lib/logs/console/logger'
22
import { isReference, parseReferencePath, REFERENCE } from '@/executor/consts'
33
import { extractBaseBlockId, extractBranchIndex } from '@/executor/utils/subflow-utils'
4-
import type { ResolutionContext, Resolver } from '@/executor/variables/resolvers/reference'
4+
import {
5+
navigatePath,
6+
type ResolutionContext,
7+
type Resolver,
8+
} from '@/executor/variables/resolvers/reference'
59
import type { SerializedWorkflow } from '@/serializer/types'
610

711
const logger = createLogger('ParallelResolver')
@@ -28,7 +32,7 @@ export class ParallelResolver implements Resolver {
2832
return undefined
2933
}
3034

31-
const [_, property] = parts
35+
const [_, property, ...pathParts] = parts
3236
const parallelId = this.findParallelForBlock(context.currentNodeId)
3337
if (!parallelId) {
3438
return undefined
@@ -47,25 +51,36 @@ export class ParallelResolver implements Resolver {
4751

4852
const distributionItems = this.getDistributionItems(parallelConfig)
4953

54+
let value: any
5055
switch (property) {
5156
case 'index':
52-
return branchIndex
57+
value = branchIndex
58+
break
5359
case 'currentItem':
5460
if (Array.isArray(distributionItems)) {
55-
return distributionItems[branchIndex]
56-
}
57-
if (typeof distributionItems === 'object' && distributionItems !== null) {
61+
value = distributionItems[branchIndex]
62+
} else if (typeof distributionItems === 'object' && distributionItems !== null) {
5863
const keys = Object.keys(distributionItems)
5964
const key = keys[branchIndex]
60-
return key !== undefined ? distributionItems[key] : undefined
65+
value = key !== undefined ? distributionItems[key] : undefined
66+
} else {
67+
return undefined
6168
}
62-
return undefined
69+
break
6370
case 'items':
64-
return distributionItems
71+
value = distributionItems
72+
break
6573
default:
6674
logger.warn('Unknown parallel property', { property })
6775
return undefined
6876
}
77+
78+
// If there are additional path parts, navigate deeper
79+
if (pathParts.length > 0) {
80+
return navigatePath(value, pathParts)
81+
}
82+
83+
return value
6984
}
7085

7186
private findParallelForBlock(blockId: string): string | undefined {

apps/sim/executor/variables/resolvers/reference.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,41 @@ export interface Resolver {
1111
canResolve(reference: string): boolean
1212
resolve(reference: string, context: ResolutionContext): any
1313
}
14+
15+
/**
16+
* Navigate through nested object properties using a path array.
17+
* Supports dot notation and array indices.
18+
*
19+
* @example
20+
* navigatePath({a: {b: {c: 1}}}, ['a', 'b', 'c']) => 1
21+
* navigatePath({items: [{name: 'test'}]}, ['items', '0', 'name']) => 'test'
22+
*/
23+
export function navigatePath(obj: any, path: string[]): any {
24+
let current = obj
25+
for (const part of path) {
26+
if (current === null || current === undefined) {
27+
return undefined
28+
}
29+
30+
// Handle array indexing like "items[0]" or just numeric indices
31+
const arrayMatch = part.match(/^([^[]+)\[(\d+)\](.*)$/)
32+
if (arrayMatch) {
33+
// Handle complex array access like "items[0]"
34+
const [, prop, index] = arrayMatch
35+
current = current[prop]
36+
if (current === undefined || current === null) {
37+
return undefined
38+
}
39+
const idx = Number.parseInt(index, 10)
40+
current = Array.isArray(current) ? current[idx] : undefined
41+
} else if (/^\d+$/.test(part)) {
42+
// Handle plain numeric index
43+
const index = Number.parseInt(part, 10)
44+
current = Array.isArray(current) ? current[index] : undefined
45+
} else {
46+
// Handle regular property access
47+
current = current[part]
48+
}
49+
}
50+
return current
51+
}

apps/sim/executor/variables/resolvers/workflow.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { createLogger } from '@/lib/logs/console/logger'
22
import { VariableManager } from '@/lib/variables/variable-manager'
33
import { isReference, parseReferencePath, REFERENCE } from '@/executor/consts'
4-
import type { ResolutionContext, Resolver } from '@/executor/variables/resolvers/reference'
4+
import {
5+
navigatePath,
6+
type ResolutionContext,
7+
type Resolver,
8+
} from '@/executor/variables/resolvers/reference'
59

610
const logger = createLogger('WorkflowResolver')
711

@@ -27,23 +31,31 @@ export class WorkflowResolver implements Resolver {
2731
return undefined
2832
}
2933

30-
const [_, variableName] = parts
34+
const [_, variableName, ...pathParts] = parts
3135

3236
const workflowVars = context.executionContext.workflowVariables || this.workflowVariables
3337

3438
for (const varObj of Object.values(workflowVars)) {
3539
const v = varObj as any
3640
if (v && (v.name === variableName || v.id === variableName)) {
3741
const normalizedType = (v.type === 'string' ? 'plain' : v.type) || 'plain'
42+
let value: any
3843
try {
39-
return VariableManager.resolveForExecution(v.value, normalizedType)
44+
value = VariableManager.resolveForExecution(v.value, normalizedType)
4045
} catch (error) {
4146
logger.warn('Failed to resolve workflow variable, returning raw value', {
4247
variableName,
4348
error: (error as Error).message,
4449
})
45-
return v.value
50+
value = v.value
4651
}
52+
53+
// If there are additional path parts, navigate deeper
54+
if (pathParts.length > 0) {
55+
return navigatePath(value, pathParts)
56+
}
57+
58+
return value
4759
}
4860
}
4961

0 commit comments

Comments
 (0)