136 lines
4.5 KiB
TypeScript
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}`;
|
|
}
|