Files
govoplan-core/webui/src/definitionGraph.ts

148 lines
4.3 KiB
TypeScript

export type DefinitionGraphPosition = {
x: number;
y: number;
};
export type DefinitionGraphPort = {
id: string;
label: string;
required: boolean;
multiple: boolean;
minimum_connections: number;
};
export type DefinitionGraphConfigField = {
id: string;
label: string;
kind: string;
required: boolean;
description?: string | null;
options: [string, string][];
};
export type DefinitionGraphNodeType = {
type: string;
category: string;
category_label: string;
label: string;
description: string;
icon: string;
input_ports: DefinitionGraphPort[];
output_ports: DefinitionGraphPort[];
config_fields: DefinitionGraphConfigField[];
default_config: Record<string, unknown>;
metadata?: Record<string, unknown>;
};
export type DefinitionGraphNode = {
id: string;
type: string;
label: string;
position: DefinitionGraphPosition;
config: Record<string, unknown>;
};
export type DefinitionGraphEdge = {
id: string;
source: string;
target: string;
source_port?: string;
target_port?: string;
};
export type DefinitionGraph = {
schema_version: number;
nodes: DefinitionGraphNode[];
edges: DefinitionGraphEdge[];
};
export type DefinitionGraphConnection = {
source: string;
target: string;
sourcePort?: string | null;
targetPort?: string | null;
};
export function createDefinitionGraphNode<TNode extends DefinitionGraphNode = DefinitionGraphNode>(
type: string,
position: DefinitionGraphPosition,
library: DefinitionGraphNodeType[]
): TNode {
const definition = library.find((item) => item.type === type);
return {
id: `${type.replace(/\./g, "-")}-${crypto.randomUUID()}`,
type,
label: definition?.label ?? type,
position,
config: structuredClone(definition?.default_config ?? {})
} as TNode;
}
export function definitionConnectionError(
graph: DefinitionGraph,
library: DefinitionGraphNodeType[],
connection: DefinitionGraphConnection,
options: { allowCycles?: boolean } = {}
): string | null {
if (!connection.source || !connection.target) return "Both endpoints are required.";
if (connection.source === connection.target) return "A node cannot connect to itself.";
const source = graph.nodes.find((node) => node.id === connection.source);
const target = graph.nodes.find((node) => node.id === connection.target);
if (!source || !target) return "The connection references an unknown node.";
const sourceDefinition = library.find((item) => item.type === source.type);
const targetDefinition = library.find((item) => item.type === target.type);
if (!sourceDefinition || !targetDefinition) return "The node type is not in this library.";
const sourcePort = connection.sourcePort ?? "output";
const targetPort = connection.targetPort ?? "input";
if (!sourceDefinition.output_ports.some((port) => port.id === sourcePort)) {
return "The source port is not available.";
}
const port = targetDefinition.input_ports.find((item) => item.id === targetPort);
if (!port) return "The target port is not available.";
if (graph.edges.some(
(edge) =>
edge.source === connection.source
&& edge.target === connection.target
&& (edge.source_port ?? "output") === sourcePort
&& (edge.target_port ?? "input") === targetPort
)) {
return "This connection already exists.";
}
if (!port.multiple && graph.edges.some(
(edge) =>
edge.target === connection.target
&& (edge.target_port ?? "input") === targetPort
)) {
return "The target port accepts only one connection.";
}
if (!options.allowCycles && wouldCreateDefinitionCycle(
graph,
connection.source,
connection.target
)) {
return "This connection would create a cycle.";
}
return null;
}
export function wouldCreateDefinitionCycle(
graph: DefinitionGraph,
source: string,
target: string
): boolean {
const outgoing = new Map<string, string[]>();
graph.edges.forEach((edge) => {
outgoing.set(edge.source, [...(outgoing.get(edge.source) ?? []), edge.target]);
});
const pending = [target];
const visited = new Set<string>();
while (pending.length) {
const nodeId = pending.pop();
if (!nodeId || visited.has(nodeId)) continue;
if (nodeId === source) return true;
visited.add(nodeId);
pending.push(...(outgoing.get(nodeId) ?? []));
}
return false;
}