/** * Automation2 Flow Editor - Data reference format and helpers. * All dynamic values use structured ref/value objects, not plain strings. */ /** Structured reference to another node's output (path = JSON path segments) */ export interface DataRef { type: 'ref'; nodeId: string; path: (string | number)[]; /** Optional declared type at bind time (for UI / validation hints) */ expectedType?: string; } /** Explicit static value wrapper */ export interface DataValue { type: 'value'; value: unknown; } /** System variable reference */ export interface SystemVarRef { type: 'system'; variable: string; } /** Union: reference, static value, or system variable */ export type DynamicValue = DataRef | DataValue | SystemVarRef; /** Type guards */ export function isSystemVar(v: unknown): v is SystemVarRef { return ( typeof v === 'object' && v !== null && (v as SystemVarRef).type === 'system' && typeof (v as SystemVarRef).variable === 'string' ); } export function isRef(v: unknown): v is DataRef { return ( typeof v === 'object' && v !== null && (v as DataRef).type === 'ref' && typeof (v as DataRef).nodeId === 'string' && Array.isArray((v as DataRef).path) ); } export function isValue(v: unknown): v is DataValue { return ( typeof v === 'object' && v !== null && (v as DataValue).type === 'value' ); } export function isDynamicValue(v: unknown): v is DynamicValue { return isRef(v) || isValue(v) || isSystemVar(v); } /** Create a system variable reference */ export function createSystemVar(variable: string): SystemVarRef { return { type: 'system', variable }; } /** Create a reference object */ export function createRef(nodeId: string, path: (string | number)[] = [], expectedType?: string): DataRef { return { type: 'ref', nodeId, path, ...(expectedType ? { expectedType } : {}) }; } /** * Structural type compatibility using the canonical type vocabulary: str / int / float / bool / Any. * All node parameters and form field schemas must use these types (no `string`, `number`, `boolean` * aliases) so no alias-mapping is needed here. * * `Any` as expected type accepts everything. * `Any`, `object`, or `dict` as produced type coerces to `str` (backend serializes via json.dumps). */ export function isCompatible(producedType: string, expectedType: string): 'ok' | 'coerce' | 'mismatch' { if (!expectedType || !producedType) return 'ok'; if (producedType === expectedType) return 'ok'; // Any-expected: accept all sources if (expectedType === 'Any') return 'ok'; // Any-produced: compatible with everything (coerce where needed) if (producedType === 'Any') return 'coerce'; // Numeric coercion if (expectedType === 'str' && (producedType === 'int' || producedType === 'float')) return 'coerce'; if (expectedType === 'int' && producedType === 'str') return 'coerce'; // Object/dict → str: backend serializes to JSON text if (expectedType === 'str' && (producedType === 'object' || producedType === 'dict')) return 'coerce'; return 'mismatch'; } /** Create a value wrapper */ export function createValue(value: unknown): DataValue { return { type: 'value', value }; } /** Resolve a ref against nodeOutputsPreview for UI preview; returns resolved value or undefined if missing */ export function resolvePreview( ref: DataRef, nodeOutputsPreview: Record ): unknown { const root = nodeOutputsPreview[ref.nodeId]; if (root === undefined) return undefined; let current: unknown = root; for (const seg of ref.path) { if (current == null) return undefined; const key = typeof seg === 'number' ? String(seg) : seg; if (Array.isArray(current) && /^\d+$/.test(key)) { const idx = parseInt(key, 10); if (idx >= 0 && idx < current.length) current = current[idx]; else return undefined; } else if (typeof current === 'object' && key in current) { current = (current as Record)[key]; } else return undefined; } return current; } /** Format a ref for human display: "Node Title → path.segment" */ export function formatRefLabel( ref: DataRef, nodes: Array<{ id: string; title?: string }>, nodeLabelFallback?: (nodeId: string) => string ): string { const node = nodes.find((n) => n.id === ref.nodeId); const nodeLabel = node?.title?.trim() || nodeLabelFallback?.(ref.nodeId) || ref.nodeId; if (ref.path.length === 0) return nodeLabel; const pathStr = ref.path.map((p) => String(p)).join(' → '); return `${nodeLabel} → ${pathStr}`; }