mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 22:48:14 -05:00
fix(env-vars): remove regex parsing from table subblock, add formatDisplayText to various subblocks that didn't have it (#1582)
This commit is contained in:
@@ -9,6 +9,7 @@ import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
|
||||
import { MAX_TAG_SLOTS } from '@/lib/knowledge/consts'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions'
|
||||
import { useTagSelection } from '@/hooks/use-tag-selection'
|
||||
@@ -40,6 +41,7 @@ export function DocumentTagEntry({
|
||||
isConnecting = false,
|
||||
}: DocumentTagEntryProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlock.id)
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
|
||||
// Get the knowledge base ID from other sub-blocks
|
||||
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
@@ -301,7 +303,12 @@ export function DocumentTagEntry({
|
||||
)}
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
|
||||
<div className='whitespace-pre'>{formatDisplayText(cellValue)}</div>
|
||||
<div className='whitespace-pre'>
|
||||
{formatDisplayText(cellValue, {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{showDropdown && availableTagDefinitions.length > 0 && (
|
||||
<div className='absolute top-full left-0 z-[100] mt-1 w-full'>
|
||||
@@ -389,7 +396,10 @@ export function DocumentTagEntry({
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
|
||||
<div className='whitespace-pre text-muted-foreground'>
|
||||
{formatDisplayText(cellValue)}
|
||||
{formatDisplayText(cellValue, {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{showTypeDropdown && !isReadOnly && (
|
||||
@@ -469,7 +479,12 @@ export function DocumentTagEntry({
|
||||
className='w-full border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
|
||||
<div className='whitespace-pre'>{formatDisplayText(cellValue)}</div>
|
||||
<div className='whitespace-pre'>
|
||||
{formatDisplayText(cellValue, {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -239,7 +239,12 @@ export function KnowledgeTagFilters({
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
|
||||
<div className='whitespace-pre'>{formatDisplayText(cellValue || 'Select tag')}</div>
|
||||
<div className='whitespace-pre'>
|
||||
{formatDisplayText(cellValue || 'Select tag', {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{showDropdown && tagDefinitions.length > 0 && (
|
||||
<div className='absolute top-full left-0 z-[100] mt-1 w-full'>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { formatDisplayText } from '@/components/ui/formatted-text'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
@@ -14,6 +15,7 @@ import { Switch } from '@/components/ui/switch'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import { useMcpTools } from '@/hooks/use-mcp-tools'
|
||||
import { formatParameterLabel } from '@/tools/params'
|
||||
|
||||
@@ -37,6 +39,7 @@ export function McpDynamicArgs({
|
||||
const { mcpTools } = useMcpTools(workspaceId)
|
||||
const [selectedTool] = useSubBlockValue(blockId, 'tool')
|
||||
const [toolArgs, setToolArgs] = useSubBlockValue(blockId, subBlockId)
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
|
||||
const selectedToolConfig = mcpTools.find((tool) => tool.id === selectedTool)
|
||||
const toolSchema = selectedToolConfig?.inputSchema
|
||||
@@ -180,7 +183,7 @@ export function McpDynamicArgs({
|
||||
|
||||
case 'long-input':
|
||||
return (
|
||||
<div key={`${paramName}-long`}>
|
||||
<div key={`${paramName}-long`} className='relative'>
|
||||
<Textarea
|
||||
value={value || ''}
|
||||
onChange={(e) => updateParameter(paramName, e.target.value, paramSchema)}
|
||||
@@ -192,8 +195,14 @@ export function McpDynamicArgs({
|
||||
}
|
||||
disabled={disabled}
|
||||
rows={4}
|
||||
className='min-h-[80px] resize-none'
|
||||
className='min-h-[80px] resize-none text-transparent caret-foreground'
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0 overflow-auto whitespace-pre-wrap break-words p-3 text-sm'>
|
||||
{formatDisplayText(value || '', {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -203,9 +212,10 @@ export function McpDynamicArgs({
|
||||
paramName.toLowerCase().includes('password') ||
|
||||
paramName.toLowerCase().includes('token')
|
||||
const isNumeric = paramSchema.type === 'number' || paramSchema.type === 'integer'
|
||||
const isTextInput = !isPassword && !isNumeric
|
||||
|
||||
return (
|
||||
<div key={`${paramName}-short`}>
|
||||
<div key={`${paramName}-short`} className={isTextInput ? 'relative' : ''}>
|
||||
<Input
|
||||
type={isPassword ? 'password' : isNumeric ? 'number' : 'text'}
|
||||
value={value || ''}
|
||||
@@ -231,7 +241,18 @@ export function McpDynamicArgs({
|
||||
`Enter ${formatParameterLabel(paramName).toLowerCase()}`
|
||||
}
|
||||
disabled={disabled}
|
||||
className={isTextInput ? 'text-transparent caret-foreground' : ''}
|
||||
/>
|
||||
{isTextInput && (
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
|
||||
<div className='whitespace-pre'>
|
||||
{formatDisplayText(value?.toString() || '', {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
|
||||
interface TableProps {
|
||||
blockId: string
|
||||
@@ -34,6 +35,7 @@ export function Table({
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<TableRow[]>(blockId, subBlockId)
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value
|
||||
const value = isPreview ? previewValue : storeValue
|
||||
@@ -240,7 +242,12 @@ export function Table({
|
||||
data-overlay={cellKey}
|
||||
className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'
|
||||
>
|
||||
<div className='whitespace-pre'>{formatDisplayText(cellValue)}</div>
|
||||
<div className='whitespace-pre'>
|
||||
{formatDisplayText(cellValue, {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import { formatDisplayText } from '@/components/ui/formatted-text'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
@@ -23,9 +24,11 @@ import {
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
interface TriggerConfigSectionProps {
|
||||
blockId: string
|
||||
triggerDef: TriggerConfig
|
||||
config: Record<string, any>
|
||||
onChange: (fieldId: string, value: any) => void
|
||||
@@ -34,6 +37,7 @@ interface TriggerConfigSectionProps {
|
||||
}
|
||||
|
||||
export function TriggerConfigSection({
|
||||
blockId,
|
||||
triggerDef,
|
||||
config,
|
||||
onChange,
|
||||
@@ -42,6 +46,7 @@ export function TriggerConfigSection({
|
||||
}: TriggerConfigSectionProps) {
|
||||
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({})
|
||||
const [copied, setCopied] = useState<string | null>(null)
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
|
||||
const copyToClipboard = (text: string, type: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
@@ -258,9 +263,20 @@ export function TriggerConfigSection({
|
||||
className={cn(
|
||||
'h-9 rounded-[8px]',
|
||||
isSecret ? 'pr-32' : '',
|
||||
'focus-visible:ring-2 focus-visible:ring-primary/20'
|
||||
'focus-visible:ring-2 focus-visible:ring-primary/20',
|
||||
!isSecret && 'text-transparent caret-foreground'
|
||||
)}
|
||||
/>
|
||||
{!isSecret && (
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
|
||||
<div className='whitespace-pre'>
|
||||
{formatDisplayText(value?.toString() || '', {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isSecret && (
|
||||
<div className='absolute top-0.5 right-0.5 flex h-8 items-center gap-1 pr-1'>
|
||||
<Button
|
||||
|
||||
@@ -467,6 +467,7 @@ export function TriggerModal({
|
||||
)}
|
||||
|
||||
<TriggerConfigSection
|
||||
blockId={blockId}
|
||||
triggerDef={triggerDef}
|
||||
config={config}
|
||||
onChange={handleConfigChange}
|
||||
|
||||
@@ -165,7 +165,7 @@ describe('ConditionBlockHandler', () => {
|
||||
mockContext,
|
||||
mockBlock
|
||||
)
|
||||
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('context.value > 5', true)
|
||||
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('context.value > 5')
|
||||
expect(result).toEqual(expectedOutput)
|
||||
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
|
||||
})
|
||||
@@ -205,7 +205,7 @@ describe('ConditionBlockHandler', () => {
|
||||
mockContext,
|
||||
mockBlock
|
||||
)
|
||||
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('context.value < 0', true)
|
||||
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('context.value < 0')
|
||||
expect(result).toEqual(expectedOutput)
|
||||
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('else1')
|
||||
})
|
||||
@@ -241,7 +241,7 @@ describe('ConditionBlockHandler', () => {
|
||||
mockContext,
|
||||
mockBlock
|
||||
)
|
||||
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('10 > 5', true)
|
||||
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('10 > 5')
|
||||
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
|
||||
})
|
||||
|
||||
@@ -268,7 +268,7 @@ describe('ConditionBlockHandler', () => {
|
||||
mockContext,
|
||||
mockBlock
|
||||
)
|
||||
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('"john" !== null', true)
|
||||
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('"john" !== null')
|
||||
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
|
||||
})
|
||||
|
||||
@@ -295,7 +295,7 @@ describe('ConditionBlockHandler', () => {
|
||||
mockContext,
|
||||
mockBlock
|
||||
)
|
||||
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('{{POOP}} === "hi"', true)
|
||||
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('{{POOP}} === "hi"')
|
||||
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
|
||||
})
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ export class ConditionBlockHandler implements BlockHandler {
|
||||
// Use full resolution pipeline: variables -> block references -> env vars
|
||||
const resolvedVars = this.resolver.resolveVariableReferences(conditionValueString, block)
|
||||
const resolvedRefs = this.resolver.resolveBlockReferences(resolvedVars, context, block)
|
||||
resolvedConditionValue = this.resolver.resolveEnvVariables(resolvedRefs, true)
|
||||
resolvedConditionValue = this.resolver.resolveEnvVariables(resolvedRefs)
|
||||
logger.info(
|
||||
`Resolved condition "${condition.title}" (${condition.id}): from "${conditionValueString}" to "${resolvedConditionValue}"`
|
||||
)
|
||||
|
||||
@@ -432,7 +432,7 @@ describe('InputResolver', () => {
|
||||
|
||||
expect(result.apiKey).toBe('test-api-key')
|
||||
expect(result.url).toBe('https://example.com?key=test-api-key')
|
||||
expect(result.regularParam).toBe('Base URL is: {{BASE_URL}}')
|
||||
expect(result.regularParam).toBe('Base URL is: https://api.example.com')
|
||||
})
|
||||
|
||||
it('should resolve explicit environment variables', () => {
|
||||
@@ -458,7 +458,7 @@ describe('InputResolver', () => {
|
||||
expect(result.explicitEnv).toBe('https://api.example.com')
|
||||
})
|
||||
|
||||
it('should not resolve environment variables in regular contexts', () => {
|
||||
it('should resolve environment variables in all contexts', () => {
|
||||
const block: SerializedBlock = {
|
||||
id: 'test-block',
|
||||
metadata: { id: 'generic', name: 'Test Block' },
|
||||
@@ -478,7 +478,7 @@ describe('InputResolver', () => {
|
||||
|
||||
const result = resolver.resolveInputs(block, mockContext)
|
||||
|
||||
expect(result.regularParam).toBe('Value with {{API_KEY}} embedded')
|
||||
expect(result.regularParam).toBe('Value with test-api-key embedded')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -313,7 +313,7 @@ export class InputResolver {
|
||||
}
|
||||
// Handle objects and arrays recursively
|
||||
else if (typeof value === 'object') {
|
||||
result[key] = this.processObjectValue(value, key, context, block)
|
||||
result[key] = this.processObjectValue(value, context, block)
|
||||
}
|
||||
// Pass through other value types
|
||||
else {
|
||||
@@ -684,7 +684,7 @@ export class InputResolver {
|
||||
}
|
||||
|
||||
// Standard block reference resolution with connection validation
|
||||
const validation = this.validateBlockReference(blockRef, currentBlock.id, context)
|
||||
const validation = this.validateBlockReference(blockRef, currentBlock.id)
|
||||
|
||||
if (!validation.isValid) {
|
||||
throw new Error(validation.errorMessage!)
|
||||
@@ -845,142 +845,42 @@ export class InputResolver {
|
||||
return resolvedValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a match with < and > is actually a variable reference.
|
||||
* Valid variable references must:
|
||||
* - Have no space after the opening <
|
||||
* - Contain a dot (.)
|
||||
* - Have no spaces until the closing >
|
||||
* - Not be comparison operators or HTML tags
|
||||
*
|
||||
* @param match - The matched string including < and >
|
||||
* @returns Whether this is a valid variable reference
|
||||
*/
|
||||
private isValidVariableReference(match: string): boolean {
|
||||
const innerContent = match.slice(1, -1)
|
||||
|
||||
if (innerContent.startsWith(' ')) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (innerContent.match(/^\s*[<>=!]+\s*$/) || innerContent.match(/\s[<>=!]+\s/)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (innerContent.match(/^[<>=!]+\s/)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (innerContent.includes('.')) {
|
||||
const dotIndex = innerContent.indexOf('.')
|
||||
const beforeDot = innerContent.substring(0, dotIndex)
|
||||
const afterDot = innerContent.substring(dotIndex + 1)
|
||||
|
||||
if (afterDot.includes(' ')) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (beforeDot.match(/[+*/=<>!]/) || afterDot.match(/[+\-*/=<>!]/)) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
innerContent.match(/[+\-*/=<>!]/) ||
|
||||
innerContent.match(/^\d/) ||
|
||||
innerContent.match(/\s\d/)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a string contains a properly formatted environment variable reference.
|
||||
* Valid references are either:
|
||||
* 1. A standalone env var (entire string is just {{ENV_VAR}})
|
||||
* 2. An explicit env var with clear boundaries (usually within a URL or similar)
|
||||
*
|
||||
* @param value - The string to check
|
||||
* @returns Whether this contains a properly formatted env var reference
|
||||
*/
|
||||
private containsProperEnvVarReference(value: string): boolean {
|
||||
if (!value || typeof value !== 'string') return false
|
||||
|
||||
// Case 1: String is just a single environment variable
|
||||
if (value.trim().match(/^\{\{[^{}]+\}\}$/)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Case 2: Check for environment variables in specific contexts
|
||||
// For example, in URLs, bearer tokens, etc.
|
||||
const properContextPatterns = [
|
||||
// Auth header patterns
|
||||
/Bearer\s+\{\{[^{}]+\}\}/i,
|
||||
/Authorization:\s+Bearer\s+\{\{[^{}]+\}\}/i,
|
||||
/Authorization:\s+\{\{[^{}]+\}\}/i,
|
||||
|
||||
// API key in URL patterns
|
||||
/[?&]api[_-]?key=\{\{[^{}]+\}\}/i,
|
||||
/[?&]key=\{\{[^{}]+\}\}/i,
|
||||
/[?&]token=\{\{[^{}]+\}\}/i,
|
||||
|
||||
// API key in header patterns
|
||||
/X-API-Key:\s+\{\{[^{}]+\}\}/i,
|
||||
/api[_-]?key:\s+\{\{[^{}]+\}\}/i,
|
||||
]
|
||||
|
||||
return properContextPatterns.some((pattern) => pattern.test(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves environment variables in any value ({{ENV_VAR}}).
|
||||
* Only processes environment variables in apiKey fields or when explicitly needed.
|
||||
*
|
||||
* @param value - Value that may contain environment variable references
|
||||
* @param isApiKey - Whether this is an API key field (requires special env var handling)
|
||||
* @returns Value with environment variables resolved
|
||||
* @throws Error if referenced environment variable is not found
|
||||
*/
|
||||
resolveEnvVariables(value: any, isApiKey = false): any {
|
||||
resolveEnvVariables(value: any): any {
|
||||
if (typeof value === 'string') {
|
||||
// Only process environment variables if:
|
||||
// 1. This is an API key field
|
||||
// 2. String is a complete environment variable reference ({{ENV_VAR}})
|
||||
// 3. String contains environment variable references in proper contexts (auth headers, URLs)
|
||||
const isExplicitEnvVar = value.trim().startsWith('{{') && value.trim().endsWith('}}')
|
||||
const hasProperEnvVarReferences = this.containsProperEnvVarReference(value)
|
||||
const envMatches = value.match(/\{\{([^}]+)\}\}/g)
|
||||
if (envMatches) {
|
||||
let resolvedValue = value
|
||||
for (const match of envMatches) {
|
||||
const envKey = match.slice(2, -2)
|
||||
const envValue = this.environmentVariables[envKey]
|
||||
|
||||
if (isApiKey || isExplicitEnvVar || hasProperEnvVarReferences) {
|
||||
const envMatches = value.match(/\{\{([^}]+)\}\}/g)
|
||||
if (envMatches) {
|
||||
let resolvedValue = value
|
||||
for (const match of envMatches) {
|
||||
const envKey = match.slice(2, -2)
|
||||
const envValue = this.environmentVariables[envKey]
|
||||
|
||||
if (envValue === undefined) {
|
||||
throw new Error(`Environment variable "${envKey}" was not found.`)
|
||||
}
|
||||
|
||||
resolvedValue = resolvedValue.replace(match, envValue)
|
||||
if (envValue === undefined) {
|
||||
throw new Error(`Environment variable "${envKey}" was not found.`)
|
||||
}
|
||||
return resolvedValue
|
||||
|
||||
resolvedValue = resolvedValue.replace(match, envValue)
|
||||
}
|
||||
return resolvedValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => this.resolveEnvVariables(item, isApiKey))
|
||||
return value.map((item) => this.resolveEnvVariables(item))
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.entries(value).reduce(
|
||||
(acc, [k, v]) => ({
|
||||
...acc,
|
||||
[k]: this.resolveEnvVariables(v, k.toLowerCase() === 'apikey'),
|
||||
[k]: this.resolveEnvVariables(v),
|
||||
}),
|
||||
{}
|
||||
)
|
||||
@@ -1016,11 +916,8 @@ export class InputResolver {
|
||||
// Then resolve block references
|
||||
const resolvedReferences = this.resolveBlockReferences(resolvedVars, context, currentBlock)
|
||||
|
||||
// Check if this is an API key field
|
||||
const isApiKey = this.isApiKeyField(currentBlock, value)
|
||||
|
||||
// Then resolve environment variables with the API key flag
|
||||
return this.resolveEnvVariables(resolvedReferences, isApiKey)
|
||||
// Then resolve environment variables
|
||||
return this.resolveEnvVariables(resolvedReferences)
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
@@ -1032,7 +929,6 @@ export class InputResolver {
|
||||
if (typeof value === 'object') {
|
||||
const result: Record<string, any> = {}
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
const _isApiKey = k.toLowerCase() === 'apikey'
|
||||
result[k] = this.resolveNestedStructure(v, context, currentBlock)
|
||||
}
|
||||
return result
|
||||
@@ -1042,38 +938,6 @@ export class InputResolver {
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given field in a block is an API key field.
|
||||
*
|
||||
* @param block - Block containing the field
|
||||
* @param value - Value to check
|
||||
* @returns Whether this appears to be an API key field
|
||||
*/
|
||||
private isApiKeyField(block: SerializedBlock, value: string): boolean {
|
||||
// Check if the block is an API or agent block (which typically have API keys)
|
||||
const blockType = block.metadata?.id
|
||||
if (blockType !== 'api' && blockType !== 'agent') {
|
||||
return false
|
||||
}
|
||||
|
||||
// Look for the value in the block params
|
||||
for (const [key, paramValue] of Object.entries(block.config.params)) {
|
||||
if (paramValue === value) {
|
||||
// Check if key name suggests it's an API key
|
||||
const normalizedKey = key.toLowerCase().replace(/[_\-\s]/g, '')
|
||||
return (
|
||||
normalizedKey === 'apikey' ||
|
||||
normalizedKey.includes('apikey') ||
|
||||
normalizedKey.includes('secretkey') ||
|
||||
normalizedKey.includes('accesskey') ||
|
||||
normalizedKey.includes('token')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a value for use in condition blocks.
|
||||
* Handles strings, null, undefined, and objects appropriately.
|
||||
@@ -1291,41 +1155,17 @@ export class InputResolver {
|
||||
return [...new Set(names)] // Remove duplicates
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a block reference could potentially be valid without throwing errors.
|
||||
* Used to filter out non-block patterns like <test> from block reference resolution.
|
||||
*
|
||||
* @param blockRef - The block reference to check
|
||||
* @param currentBlockId - ID of the current block
|
||||
* @returns Whether this could be a valid block reference
|
||||
*/
|
||||
private isAccessibleBlockReference(blockRef: string, currentBlockId: string): boolean {
|
||||
// Special cases that are always allowed
|
||||
const specialRefs = ['start', 'loop', 'parallel']
|
||||
if (specialRefs.includes(blockRef.toLowerCase())) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Get all accessible block names for this block
|
||||
const accessibleNames = this.getAccessibleBlockNames(currentBlockId)
|
||||
|
||||
// Check if the reference matches any accessible block name
|
||||
return accessibleNames.includes(blockRef) || accessibleNames.includes(blockRef.toLowerCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a block reference is accessible from the current block.
|
||||
* Checks existence and connection-based access rules.
|
||||
*
|
||||
* @param blockRef - Name or ID of the referenced block
|
||||
* @param currentBlockId - ID of the block making the reference
|
||||
* @param context - Current execution context
|
||||
* @returns Validation result with success status and resolved block ID or error message
|
||||
*/
|
||||
private validateBlockReference(
|
||||
blockRef: string,
|
||||
currentBlockId: string,
|
||||
context: ExecutionContext
|
||||
currentBlockId: string
|
||||
): { isValid: boolean; resolvedBlockId?: string; errorMessage?: string } {
|
||||
// Special case: 'start' is always allowed
|
||||
if (blockRef.toLowerCase() === 'start') {
|
||||
@@ -1709,7 +1549,7 @@ export class InputResolver {
|
||||
} else if (parallel.nodes.includes(currentBlock.id)) {
|
||||
// Fallback: if we're inside a parallel execution but don't have currentVirtualBlockId
|
||||
// This shouldn't happen in normal execution but provides backward compatibility
|
||||
for (const [virtualId, mapping] of context.parallelBlockMapping || new Map()) {
|
||||
for (const [_, mapping] of context.parallelBlockMapping || new Map()) {
|
||||
if (mapping.originalBlockId === currentBlock.id && mapping.parallelId === parallelId) {
|
||||
const iterationKey = `${parallelId}_iteration_${mapping.iterationIndex}`
|
||||
const iterationItem = context.loopItems.get(iterationKey)
|
||||
@@ -1899,11 +1739,8 @@ export class InputResolver {
|
||||
// Then resolve block references
|
||||
const resolvedReferences = this.resolveBlockReferences(resolvedVars, context, block)
|
||||
|
||||
// Check if this is an API key field
|
||||
const isApiKey = this.isApiKeyField(block, value)
|
||||
|
||||
// Then resolve environment variables
|
||||
const resolvedEnv = this.resolveEnvVariables(resolvedReferences, isApiKey)
|
||||
const resolvedEnv = this.resolveEnvVariables(resolvedReferences)
|
||||
|
||||
// Special handling for different block types
|
||||
const blockType = block.metadata?.id
|
||||
@@ -1927,17 +1764,11 @@ export class InputResolver {
|
||||
* Handles special cases like table-like arrays with cells.
|
||||
*
|
||||
* @param value - Object or array to process
|
||||
* @param key - The parameter key
|
||||
* @param context - Current execution context
|
||||
* @param block - Block containing the value
|
||||
* @returns Processed object/array
|
||||
*/
|
||||
private processObjectValue(
|
||||
value: any,
|
||||
key: string,
|
||||
context: ExecutionContext,
|
||||
block: SerializedBlock
|
||||
): any {
|
||||
private processObjectValue(value: any, context: ExecutionContext, block: SerializedBlock): any {
|
||||
// Special handling for table-like arrays (e.g., from API params/headers)
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
|
||||
Reference in New Issue
Block a user