frontend_nyla/src/components/FlowEditor/nodes/shared/dataRef.ts

136 lines
4.5 KiB
TypeScript

/**
* 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<string, unknown>
): 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<string, unknown>)[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}`;
}