improvement(variables): support dot notation for nested objects (#1992)

This commit is contained in:
Siddharth Ganesan
2025-11-14 14:47:16 -08:00
committed by GitHub
parent 72a048f37d
commit 4b4060f63f
5 changed files with 105 additions and 81 deletions

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}
}