import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import JSON5 from 'json5' import { create } from 'zustand' import { devtools } from 'zustand/middleware' import { normalizeName } from '@/executor/constants' import { useOperationQueueStore } from '@/stores/operation-queue/store' import type { Variable, VariablesStore } from '@/stores/variables/types' import { useSubBlockStore } from '@/stores/workflows/subblock/store' const logger = createLogger('VariablesStore') function validateVariable(variable: Variable): string | undefined { try { switch (variable.type) { case 'number': if (Number.isNaN(Number(variable.value))) { return 'Not a valid number' } break case 'boolean': if (!/^(true|false)$/i.test(String(variable.value).trim())) { return 'Expected "true" or "false"' } break case 'object': try { const valueToEvaluate = String(variable.value).trim() if (!valueToEvaluate.startsWith('{') || !valueToEvaluate.endsWith('}')) { return 'Not a valid object format' } const parsed = JSON5.parse(valueToEvaluate) if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { return 'Not a valid object' } return undefined } catch (e) { logger.error('Object parsing error:', e) return 'Invalid object syntax' } case 'array': try { const parsed = JSON5.parse(String(variable.value)) if (!Array.isArray(parsed)) { return 'Not a valid array' } } catch { return 'Invalid array syntax' } break } return undefined } catch (e) { return getErrorMessage(e, 'Invalid format') } } export const useVariablesStore = create()( devtools((set, get) => ({ variables: {}, isLoading: false, error: null, isEditing: null, addVariable: (variable, providedId?: string) => { const id = providedId || generateId() const workflowVariables = get().getVariablesByWorkflowId(variable.workflowId) if (!variable.name || /^variable\d+$/.test(variable.name)) { const existingNumbers = workflowVariables .map((v) => { const match = v.name.match(/^variable(\d+)$/) return match ? Number.parseInt(match[1]) : 0 }) .filter((n) => !Number.isNaN(n)) const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1 variable.name = `variable${nextNumber}` } let uniqueName = variable.name let nameIndex = 1 while (workflowVariables.some((v) => v.name === uniqueName)) { uniqueName = `${variable.name} (${nameIndex})` nameIndex++ } if (variable.type === 'string') { variable.type = 'plain' } const newVariable: Variable = { id, workflowId: variable.workflowId, name: uniqueName, type: variable.type, value: variable.value || '', validationError: undefined, } const validationError = validateVariable(newVariable) if (validationError) { newVariable.validationError = validationError } set((state) => ({ variables: { ...state.variables, [id]: newVariable, }, })) return id }, updateVariable: (id, update) => { set((state) => { if (!state.variables[id]) return state if (update.name !== undefined) { const oldVariable = state.variables[id] const oldVariableName = oldVariable.name const newName = update.name.trim() if (!newName) { update = { ...update } update.name = undefined } else if (newName !== oldVariableName) { const subBlockStore = useSubBlockStore.getState() const targetWorkflowId = oldVariable.workflowId if (targetWorkflowId) { const workflowValues = subBlockStore.workflowValues[targetWorkflowId] || {} const updatedWorkflowValues = { ...workflowValues } const changedSubBlocks: Array<{ blockId: string; subBlockId: string; value: any }> = [] const oldVarName = normalizeName(oldVariableName) const newVarName = normalizeName(newName) const regex = new RegExp(``, 'gi') const updateReferences = (value: any, pattern: RegExp, replacement: string): any => { if (typeof value === 'string') { return pattern.test(value) ? value.replace(pattern, replacement) : value } if (Array.isArray(value)) { return value.map((item) => updateReferences(item, pattern, replacement)) } if (value !== null && typeof value === 'object') { const result = { ...value } for (const key in result) { result[key] = updateReferences(result[key], pattern, replacement) } return result } return value } Object.entries(workflowValues).forEach(([blockId, blockValues]) => { Object.entries(blockValues as Record).forEach( ([subBlockId, value]) => { const updatedValue = updateReferences(value, regex, ``) if (JSON.stringify(updatedValue) !== JSON.stringify(value)) { if (!updatedWorkflowValues[blockId]) { updatedWorkflowValues[blockId] = { ...workflowValues[blockId] } } updatedWorkflowValues[blockId][subBlockId] = updatedValue changedSubBlocks.push({ blockId, subBlockId, value: updatedValue }) } } ) }) // Update local state useSubBlockStore.setState({ workflowValues: { ...subBlockStore.workflowValues, [targetWorkflowId]: updatedWorkflowValues, }, }) // Queue operations for persistence via socket const operationQueue = useOperationQueueStore.getState() for (const { blockId, subBlockId, value } of changedSubBlocks) { operationQueue.addToQueue({ id: generateId(), operation: { operation: 'subblock-update', target: 'subblock', payload: { blockId, subblockId: subBlockId, value }, }, workflowId: targetWorkflowId, userId: 'system', }) } } } } if (update.type === 'string') { update = { ...update, type: 'plain' } } const updatedVariable: Variable = { ...state.variables[id], ...update, validationError: undefined, } if (update.type || update.value !== undefined) { updatedVariable.validationError = validateVariable(updatedVariable) } const updated = { ...state.variables, [id]: updatedVariable, } return { variables: updated } }) }, deleteVariable: (id) => { set((state) => { if (!state.variables[id]) return state const { [id]: _, ...rest } = state.variables return { variables: rest } }) }, getVariablesByWorkflowId: (workflowId) => { return Object.values(get().variables).filter((variable) => variable.workflowId === workflowId) }, })) )