diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts index 4b27cf1bb2..f09a258e65 100644 --- a/apps/sim/executor/variables/resolvers/block.ts +++ b/apps/sim/executor/variables/resolvers/block.ts @@ -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' @@ -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) : [] @@ -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, diff --git a/apps/sim/executor/variables/resolvers/loop.ts b/apps/sim/executor/variables/resolvers/loop.ts index 8707df4a38..a406bab8fb 100644 --- a/apps/sim/executor/variables/resolvers/loop.ts +++ b/apps/sim/executor/variables/resolvers/loop.ts @@ -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') @@ -28,7 +32,7 @@ export class LoopResolver implements Resolver { return undefined } - const [_, property] = parts + const [_, property, ...pathParts] = parts let loopScope = context.loopScope if (!loopScope) { @@ -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 { diff --git a/apps/sim/executor/variables/resolvers/parallel.ts b/apps/sim/executor/variables/resolvers/parallel.ts index 9502ccafb8..ce6a5523b9 100644 --- a/apps/sim/executor/variables/resolvers/parallel.ts +++ b/apps/sim/executor/variables/resolvers/parallel.ts @@ -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') @@ -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 @@ -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 { diff --git a/apps/sim/executor/variables/resolvers/reference.ts b/apps/sim/executor/variables/resolvers/reference.ts index 50cfa969f4..986ee2ab64 100644 --- a/apps/sim/executor/variables/resolvers/reference.ts +++ b/apps/sim/executor/variables/resolvers/reference.ts @@ -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+)\](.*)$/) + 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 +} diff --git a/apps/sim/executor/variables/resolvers/workflow.ts b/apps/sim/executor/variables/resolvers/workflow.ts index ce7da05b06..60e8070c2e 100644 --- a/apps/sim/executor/variables/resolvers/workflow.ts +++ b/apps/sim/executor/variables/resolvers/workflow.ts @@ -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') @@ -27,7 +31,7 @@ export class WorkflowResolver implements Resolver { return undefined } - const [_, variableName] = parts + const [_, variableName, ...pathParts] = parts const workflowVars = context.executionContext.workflowVariables || this.workflowVariables @@ -35,15 +39,23 @@ export class WorkflowResolver implements Resolver { 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 } }