mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-17 18:02:09 -05:00
improvement(variables): support dot notation for nested objects (#1992)
This commit is contained in:
committed by
GitHub
parent
72a048f37d
commit
4b4060f63f
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user