Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 6 additions & 63 deletions apps/sim/executor/variables/resolvers/block.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { isReference, parseReferencePath, SPECIAL_REFERENCE_PREFIXES } from '@/executor/consts'
import type { ResolutionContext, Resolver } from '@/executor/variables/resolvers/reference'
import {
navigatePath,
type ResolutionContext,
type Resolver,
} from '@/executor/variables/resolvers/reference'
import type { SerializedWorkflow } from '@/serializer/types'
import { normalizeBlockName } from '@/stores/workflows/utils'

Expand Down Expand Up @@ -50,7 +54,7 @@ export class BlockResolver implements Resolver {
return output
}

const result = this.navigatePath(output, pathParts)
const result = navigatePath(output, pathParts)

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

private navigatePath(obj: any, path: string[]): any {
let current = obj
for (const part of path) {
if (current === null || current === undefined) {
return undefined
}

const arrayMatch = part.match(/^([^[]+)\[(\d+)\](.*)$/)
if (arrayMatch) {
current = this.resolvePartWithIndices(current, part, '', 'block')
} else if (/^\d+$/.test(part)) {
const index = Number.parseInt(part, 10)
current = Array.isArray(current) ? current[index] : undefined
} else {
current = current[part]
}
}
return current
}

private resolvePartWithIndices(
base: any,
part: string,
fullPath: string,
sourceName: string
): any {
let value = base

const propMatch = part.match(/^([^[]+)/)
let rest = part
if (propMatch) {
const prop = propMatch[1]
value = value[prop]
rest = part.slice(prop.length)
if (value === undefined) {
throw new Error(`No value found at path "${fullPath}" in block "${sourceName}".`)
}
}

const indexRe = /^\[(\d+)\]/
while (rest.length > 0) {
const m = rest.match(indexRe)
if (!m) {
throw new Error(`Invalid path "${part}" in "${fullPath}" for block "${sourceName}".`)
}
const idx = Number.parseInt(m[1], 10)
if (!Array.isArray(value)) {
throw new Error(`Invalid path "${part}" in "${fullPath}" for block "${sourceName}".`)
}
if (idx < 0 || idx >= value.length) {
throw new Error(
`Array index ${idx} out of bounds (length: ${value.length}) in path "${part}"`
)
}
value = value[idx]
rest = rest.slice(m[0].length)
}

return value
}

public formatValueForBlock(
value: any,
blockType: string | undefined,
Expand Down
26 changes: 21 additions & 5 deletions apps/sim/executor/variables/resolvers/loop.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { createLogger } from '@/lib/logs/console/logger'
import { isReference, parseReferencePath, REFERENCE } from '@/executor/consts'
import { extractBaseBlockId } from '@/executor/utils/subflow-utils'
import type { ResolutionContext, Resolver } from '@/executor/variables/resolvers/reference'
import {
navigatePath,
type ResolutionContext,
type Resolver,
} from '@/executor/variables/resolvers/reference'
import type { SerializedWorkflow } from '@/serializer/types'

const logger = createLogger('LoopResolver')
Expand All @@ -28,7 +32,7 @@ export class LoopResolver implements Resolver {
return undefined
}

const [_, property] = parts
const [_, property, ...pathParts] = parts
let loopScope = context.loopScope

if (!loopScope) {
Expand All @@ -43,19 +47,31 @@ export class LoopResolver implements Resolver {
logger.warn('Loop scope not found', { reference })
return undefined
}

let value: any
switch (property) {
case 'iteration':
case 'index':
return loopScope.iteration
value = loopScope.iteration
break
case 'item':
case 'currentItem':
return loopScope.item
value = loopScope.item
break
case 'items':
return loopScope.items
value = loopScope.items
break
default:
logger.warn('Unknown loop property', { property })
return undefined
}

// If there are additional path parts, navigate deeper
if (pathParts.length > 0) {
return navigatePath(value, pathParts)
}

return value
}

private findLoopForBlock(blockId: string): string | undefined {
Expand Down
33 changes: 24 additions & 9 deletions apps/sim/executor/variables/resolvers/parallel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { createLogger } from '@/lib/logs/console/logger'
import { isReference, parseReferencePath, REFERENCE } from '@/executor/consts'
import { extractBaseBlockId, extractBranchIndex } from '@/executor/utils/subflow-utils'
import type { ResolutionContext, Resolver } from '@/executor/variables/resolvers/reference'
import {
navigatePath,
type ResolutionContext,
type Resolver,
} from '@/executor/variables/resolvers/reference'
import type { SerializedWorkflow } from '@/serializer/types'

const logger = createLogger('ParallelResolver')
Expand All @@ -28,7 +32,7 @@ export class ParallelResolver implements Resolver {
return undefined
}

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

const distributionItems = this.getDistributionItems(parallelConfig)

let value: any
switch (property) {
case 'index':
return branchIndex
value = branchIndex
break
case 'currentItem':
if (Array.isArray(distributionItems)) {
return distributionItems[branchIndex]
}
if (typeof distributionItems === 'object' && distributionItems !== null) {
value = distributionItems[branchIndex]
} else if (typeof distributionItems === 'object' && distributionItems !== null) {
const keys = Object.keys(distributionItems)
const key = keys[branchIndex]
return key !== undefined ? distributionItems[key] : undefined
value = key !== undefined ? distributionItems[key] : undefined
} else {
return undefined
}
return undefined
break
case 'items':
return distributionItems
value = distributionItems
break
default:
logger.warn('Unknown parallel property', { property })
return undefined
}

// If there are additional path parts, navigate deeper
if (pathParts.length > 0) {
return navigatePath(value, pathParts)
}

return value
}

private findParallelForBlock(blockId: string): string | undefined {
Expand Down
38 changes: 38 additions & 0 deletions apps/sim/executor/variables/resolvers/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,41 @@ export interface Resolver {
canResolve(reference: string): boolean
resolve(reference: string, context: ResolutionContext): any
}

/**
* Navigate through nested object properties using a path array.
* Supports dot notation and array indices.
*
* @example
* navigatePath({a: {b: {c: 1}}}, ['a', 'b', 'c']) => 1
* navigatePath({items: [{name: 'test'}]}, ['items', '0', 'name']) => 'test'
*/
export function navigatePath(obj: any, path: string[]): any {
let current = obj
for (const part of path) {
if (current === null || current === undefined) {
return undefined
}

// Handle array indexing like "items[0]" or just numeric indices
const arrayMatch = part.match(/^([^[]+)\[(\d+)\](.*)$/)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Third capture group (.*) is unused - consider removing it or handling nested array access like items[0][1]

Suggested change
const arrayMatch = part.match(/^([^[]+)\[(\d+)\](.*)$/)
const arrayMatch = part.match(/^([^[]+)\[(\d+)\]$/)
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/sim/executor/variables/resolvers/reference.ts
Line: 31:31

Comment:
**style:** Third capture group `(.*)` is unused - consider removing it or handling nested array access like `items[0][1]`

```suggestion
    const arrayMatch = part.match(/^([^[]+)\[(\d+)\]$/)
```

How can I resolve this? If you propose a fix, please make it concise.

if (arrayMatch) {
// Handle complex array access like "items[0]"
const [, prop, index] = arrayMatch
current = current[prop]
if (current === undefined || current === null) {
return undefined
}
const idx = Number.parseInt(index, 10)
current = Array.isArray(current) ? current[idx] : undefined
} else if (/^\d+$/.test(part)) {
// Handle plain numeric index
const index = Number.parseInt(part, 10)
current = Array.isArray(current) ? current[index] : undefined
} else {
// Handle regular property access
current = current[part]
}
}
return current
}
20 changes: 16 additions & 4 deletions apps/sim/executor/variables/resolvers/workflow.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { createLogger } from '@/lib/logs/console/logger'
import { VariableManager } from '@/lib/variables/variable-manager'
import { isReference, parseReferencePath, REFERENCE } from '@/executor/consts'
import type { ResolutionContext, Resolver } from '@/executor/variables/resolvers/reference'
import {
navigatePath,
type ResolutionContext,
type Resolver,
} from '@/executor/variables/resolvers/reference'

const logger = createLogger('WorkflowResolver')

Expand All @@ -27,23 +31,31 @@ export class WorkflowResolver implements Resolver {
return undefined
}

const [_, variableName] = parts
const [_, variableName, ...pathParts] = parts

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

for (const varObj of Object.values(workflowVars)) {
const v = varObj as any
if (v && (v.name === variableName || v.id === variableName)) {
const normalizedType = (v.type === 'string' ? 'plain' : v.type) || 'plain'
let value: any
try {
return VariableManager.resolveForExecution(v.value, normalizedType)
value = VariableManager.resolveForExecution(v.value, normalizedType)
} catch (error) {
logger.warn('Failed to resolve workflow variable, returning raw value', {
variableName,
error: (error as Error).message,
})
return v.value
value = v.value
}

// If there are additional path parts, navigate deeper
if (pathParts.length > 0) {
return navigatePath(value, pathParts)
}

return value
}
}

Expand Down