@statelyai/graph 1.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -44
- package/dist/{adjacency-list-VsUaH9SJ.mjs → adjacency-list-DQ32Mmhx.mjs} +3 -1
- package/dist/algorithms-D1cgly0g.d.mts +452 -0
- package/dist/algorithms-DBpH74hR.mjs +3309 -0
- package/dist/algorithms.d.mts +2 -2
- package/dist/algorithms.mjs +2 -2
- package/dist/config-Dt5u1gSf.mjs +793 -0
- package/dist/{converter-udLITX36.mjs → converter-DB6Rg6Vd.mjs} +2 -2
- package/dist/format-support.mjs +38 -11
- package/dist/formats/adjacency-list/index.d.mts +1 -1
- package/dist/formats/adjacency-list/index.mjs +1 -1
- package/dist/formats/converter/index.d.mts +1 -1
- package/dist/formats/converter/index.mjs +1 -1
- package/dist/formats/cytoscape/index.d.mts +4 -4
- package/dist/formats/cytoscape/index.mjs +10 -4
- package/dist/formats/d2/index.d.mts +1 -1
- package/dist/formats/d2/index.mjs +26 -12
- package/dist/formats/d3/index.d.mts +4 -4
- package/dist/formats/d3/index.mjs +10 -4
- package/dist/formats/dot/index.d.mts +1 -1
- package/dist/formats/dot/index.mjs +22 -6
- package/dist/formats/edge-list/index.d.mts +1 -1
- package/dist/formats/edge-list/index.mjs +1 -1
- package/dist/formats/elk/index.d.mts +1 -1
- package/dist/formats/elk/index.mjs +63 -24
- package/dist/formats/gexf/index.d.mts +1 -1
- package/dist/formats/gexf/index.mjs +43 -16
- package/dist/formats/gml/index.d.mts +4 -4
- package/dist/formats/gml/index.mjs +28 -15
- package/dist/formats/graphml/index.d.mts +1 -1
- package/dist/formats/graphml/index.mjs +96 -23
- package/dist/formats/jgf/index.d.mts +4 -4
- package/dist/formats/jgf/index.mjs +12 -5
- package/dist/formats/mermaid/index.d.mts +1 -1
- package/dist/formats/mermaid/index.mjs +49 -12
- package/dist/formats/tgf/index.d.mts +4 -4
- package/dist/formats/tgf/index.mjs +4 -4
- package/dist/formats/xyflow/index.d.mts +12 -6
- package/dist/formats/xyflow/index.mjs +42 -10
- package/dist/{index-D9Kj6Fe3.d.mts → index-BlbSWUvH.d.mts} +1 -1
- package/dist/{index-CHoriXZD.d.mts → index-CNvqxPLJ.d.mts} +157 -30
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +290 -307
- package/dist/layout/cytoscape.d.mts +66 -0
- package/dist/layout/cytoscape.mjs +114 -0
- package/dist/layout/d3-force.d.mts +52 -0
- package/dist/layout/d3-force.mjs +127 -0
- package/dist/layout/d3-hierarchy.d.mts +39 -0
- package/dist/layout/d3-hierarchy.mjs +135 -0
- package/dist/layout/dagre.d.mts +32 -0
- package/dist/layout/dagre.mjs +99 -0
- package/dist/layout/elk.d.mts +47 -0
- package/dist/layout/elk.mjs +73 -0
- package/dist/layout/forceatlas2.d.mts +48 -0
- package/dist/layout/forceatlas2.mjs +100 -0
- package/dist/layout/graphviz.d.mts +50 -0
- package/dist/layout/graphviz.mjs +179 -0
- package/dist/layout/index.d.mts +185 -0
- package/dist/layout/index.mjs +181 -0
- package/dist/layout/webcola.d.mts +40 -0
- package/dist/layout/webcola.mjs +104 -0
- package/dist/{queries-BlkA1HAN.d.mts → queries-B6quF529.d.mts} +43 -12
- package/dist/queries-BMM0XAv_.mjs +986 -0
- package/dist/queries.d.mts +1 -1
- package/dist/queries.mjs +1 -768
- package/dist/schemas.d.mts +19 -1
- package/dist/schemas.mjs +32 -84
- package/dist/{types-3-FS9NV2.d.mts → types-BAEQTwK_.d.mts} +99 -7
- package/dist/validate-BsfSOv0S.mjs +190 -0
- package/package.json +59 -7
- package/schemas/edge.schema.json +27 -0
- package/schemas/graph.schema.json +27 -0
- package/dist/algorithms-Ba7o7niK.mjs +0 -2394
- package/dist/algorithms-fTqmvhzP.d.mts +0 -178
- package/dist/indexing-DR8M1vBy.mjs +0 -137
- /package/dist/{edge-list-DP4otyPU.mjs → edge-list-CA9UTvn2.mjs} +0 -0
- /package/dist/{mode-D8OnHFBk.mjs → mode-gu_mhKKs.mjs} +0 -0
package/dist/schemas.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S as GraphPort, f as Graph, h as GraphEdge, y as GraphNode } from "./types-BAEQTwK_.mjs";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
|
|
4
4
|
//#region src/schemas.d.ts
|
|
@@ -61,6 +61,15 @@ declare const EdgeSchema: z.ZodObject<{
|
|
|
61
61
|
undirected: "undirected";
|
|
62
62
|
bidirectional: "bidirectional";
|
|
63
63
|
}>>;
|
|
64
|
+
points: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
65
|
+
x: z.ZodNumber;
|
|
66
|
+
y: z.ZodNumber;
|
|
67
|
+
}, z.core.$strip>>>;
|
|
68
|
+
routing: z.ZodOptional<z.ZodEnum<{
|
|
69
|
+
polyline: "polyline";
|
|
70
|
+
orthogonal: "orthogonal";
|
|
71
|
+
splines: "splines";
|
|
72
|
+
}>>;
|
|
64
73
|
data: z.ZodAny;
|
|
65
74
|
x: z.ZodOptional<z.ZodNumber>;
|
|
66
75
|
y: z.ZodOptional<z.ZodNumber>;
|
|
@@ -121,6 +130,15 @@ declare const GraphSchema: z.ZodObject<{
|
|
|
121
130
|
undirected: "undirected";
|
|
122
131
|
bidirectional: "bidirectional";
|
|
123
132
|
}>>;
|
|
133
|
+
points: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
134
|
+
x: z.ZodNumber;
|
|
135
|
+
y: z.ZodNumber;
|
|
136
|
+
}, z.core.$strip>>>;
|
|
137
|
+
routing: z.ZodOptional<z.ZodEnum<{
|
|
138
|
+
polyline: "polyline";
|
|
139
|
+
orthogonal: "orthogonal";
|
|
140
|
+
splines: "splines";
|
|
141
|
+
}>>;
|
|
124
142
|
data: z.ZodAny;
|
|
125
143
|
x: z.ZodOptional<z.ZodNumber>;
|
|
126
144
|
y: z.ZodOptional<z.ZodNumber>;
|
package/dist/schemas.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as getGraphIssues$1 } from "./validate-BsfSOv0S.mjs";
|
|
1
2
|
import * as z from "zod";
|
|
2
3
|
|
|
3
4
|
//#region src/schemas.ts
|
|
@@ -53,6 +54,15 @@ const EdgeSchema = z.object({
|
|
|
53
54
|
sourcePort: z.string().optional(),
|
|
54
55
|
targetPort: z.string().optional(),
|
|
55
56
|
mode: ModeSchema.optional(),
|
|
57
|
+
points: z.array(z.object({
|
|
58
|
+
x: z.number(),
|
|
59
|
+
y: z.number()
|
|
60
|
+
})).optional(),
|
|
61
|
+
routing: z.enum([
|
|
62
|
+
"polyline",
|
|
63
|
+
"orthogonal",
|
|
64
|
+
"splines"
|
|
65
|
+
]).optional(),
|
|
56
66
|
data: z.any(),
|
|
57
67
|
x: z.number().optional(),
|
|
58
68
|
y: z.number().optional(),
|
|
@@ -116,37 +126,30 @@ function createIssue(code, message, path) {
|
|
|
116
126
|
path
|
|
117
127
|
};
|
|
118
128
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
/** Maps structural issue codes from `validate.ts` to this module's codes. */
|
|
130
|
+
const STRUCTURAL_CODE_MAP = {
|
|
131
|
+
"duplicate-node-id": "duplicate_node_id",
|
|
132
|
+
"duplicate-edge-id": "duplicate_edge_id",
|
|
133
|
+
"missing-initial-node": "missing_initial_node",
|
|
134
|
+
"missing-parent": "missing_parent",
|
|
135
|
+
"missing-node-initial": "missing_node_initial",
|
|
136
|
+
"duplicate-port-name": "duplicate_port_name",
|
|
137
|
+
"parent-cycle": "parent_cycle",
|
|
138
|
+
"missing-source-port": "missing_source_port",
|
|
139
|
+
"missing-target-port": "missing_target_port"
|
|
140
|
+
};
|
|
141
|
+
function toValidationIssue(issue) {
|
|
142
|
+
const path = issue.path ?? [];
|
|
143
|
+
let code = STRUCTURAL_CODE_MAP[issue.code] ?? issue.code;
|
|
144
|
+
if (issue.code === "dangling-edge-endpoint") code = path[path.length - 1] === "sourceId" ? "missing_source_node" : "missing_target_node";
|
|
145
|
+
return {
|
|
146
|
+
code,
|
|
147
|
+
message: issue.message,
|
|
148
|
+
path
|
|
149
|
+
};
|
|
130
150
|
}
|
|
131
151
|
function getGraphInvariantIssues(graph) {
|
|
132
|
-
const issues =
|
|
133
|
-
const nodeIndexes = /* @__PURE__ */ new Map();
|
|
134
|
-
const nodesById = /* @__PURE__ */ new Map();
|
|
135
|
-
for (const [id, indexes] of getDuplicateIndexes(graph.nodes, (node) => node.id)) for (const index of indexes) issues.push(createIssue("duplicate_node_id", `Duplicate node id "${id}"`, [
|
|
136
|
-
"nodes",
|
|
137
|
-
index,
|
|
138
|
-
"id"
|
|
139
|
-
]));
|
|
140
|
-
for (const [id, indexes] of getDuplicateIndexes(graph.edges, (edge) => edge.id)) for (const index of indexes) issues.push(createIssue("duplicate_edge_id", `Duplicate edge id "${id}"`, [
|
|
141
|
-
"edges",
|
|
142
|
-
index,
|
|
143
|
-
"id"
|
|
144
|
-
]));
|
|
145
|
-
graph.nodes.forEach((node, index) => {
|
|
146
|
-
nodeIndexes.set(node.id, index);
|
|
147
|
-
nodesById.set(node.id, node);
|
|
148
|
-
});
|
|
149
|
-
if (graph.initialNodeId && !nodeIndexes.has(graph.initialNodeId)) issues.push(createIssue("missing_initial_node", `Initial node "${graph.initialNodeId}" does not exist`, ["initialNodeId"]));
|
|
152
|
+
const issues = getGraphIssues$1(graph).map(toValidationIssue);
|
|
150
153
|
graph.nodes.forEach((node, index) => {
|
|
151
154
|
if (node.id === "") issues.push(createIssue("empty_node_id", "Node id must be a non-empty string", [
|
|
152
155
|
"nodes",
|
|
@@ -158,68 +161,13 @@ function getGraphInvariantIssues(graph) {
|
|
|
158
161
|
index,
|
|
159
162
|
"parentId"
|
|
160
163
|
]));
|
|
161
|
-
else if (node.parentId != null && !nodeIndexes.has(node.parentId)) issues.push(createIssue("missing_parent", `Parent node "${node.parentId}" does not exist`, [
|
|
162
|
-
"nodes",
|
|
163
|
-
index,
|
|
164
|
-
"parentId"
|
|
165
|
-
]));
|
|
166
|
-
if (node.initialNodeId && !nodeIndexes.has(node.initialNodeId)) issues.push(createIssue("missing_node_initial", `Initial node "${node.initialNodeId}" does not exist`, [
|
|
167
|
-
"nodes",
|
|
168
|
-
index,
|
|
169
|
-
"initialNodeId"
|
|
170
|
-
]));
|
|
171
|
-
for (const [name, indexes] of getDuplicateIndexes(node.ports ?? [], (port) => port.name)) for (const portIndex of indexes) issues.push(createIssue("duplicate_port_name", `Duplicate port name "${name}" on node "${node.id}"`, [
|
|
172
|
-
"nodes",
|
|
173
|
-
index,
|
|
174
|
-
"ports",
|
|
175
|
-
portIndex,
|
|
176
|
-
"name"
|
|
177
|
-
]));
|
|
178
164
|
});
|
|
179
|
-
for (const node of graph.nodes) {
|
|
180
|
-
const seen = /* @__PURE__ */ new Set();
|
|
181
|
-
let current = node.parentId;
|
|
182
|
-
while (current != null) {
|
|
183
|
-
if (current === node.id || seen.has(current)) {
|
|
184
|
-
issues.push(createIssue("parent_cycle", `Node "${node.id}" is part of a parent cycle`, [
|
|
185
|
-
"nodes",
|
|
186
|
-
nodeIndexes.get(node.id) ?? 0,
|
|
187
|
-
"parentId"
|
|
188
|
-
]));
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
seen.add(current);
|
|
192
|
-
current = nodesById.get(current)?.parentId;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
165
|
graph.edges.forEach((edge, index) => {
|
|
196
166
|
if (edge.id === "") issues.push(createIssue("empty_edge_id", "Edge id must be a non-empty string", [
|
|
197
167
|
"edges",
|
|
198
168
|
index,
|
|
199
169
|
"id"
|
|
200
170
|
]));
|
|
201
|
-
const source = nodesById.get(edge.sourceId);
|
|
202
|
-
const target = nodesById.get(edge.targetId);
|
|
203
|
-
if (!source) issues.push(createIssue("missing_source_node", `Source node "${edge.sourceId}" does not exist`, [
|
|
204
|
-
"edges",
|
|
205
|
-
index,
|
|
206
|
-
"sourceId"
|
|
207
|
-
]));
|
|
208
|
-
if (!target) issues.push(createIssue("missing_target_node", `Target node "${edge.targetId}" does not exist`, [
|
|
209
|
-
"edges",
|
|
210
|
-
index,
|
|
211
|
-
"targetId"
|
|
212
|
-
]));
|
|
213
|
-
if (source && edge.sourcePort !== void 0 && !source.ports?.some((port) => port.name === edge.sourcePort)) issues.push(createIssue("missing_source_port", `Port "${edge.sourcePort}" does not exist on source node "${edge.sourceId}"`, [
|
|
214
|
-
"edges",
|
|
215
|
-
index,
|
|
216
|
-
"sourcePort"
|
|
217
|
-
]));
|
|
218
|
-
if (target && edge.targetPort !== void 0 && !target.ports?.some((port) => port.name === edge.targetPort)) issues.push(createIssue("missing_target_port", `Port "${edge.targetPort}" does not exist on target node "${edge.targetId}"`, [
|
|
219
|
-
"edges",
|
|
220
|
-
index,
|
|
221
|
-
"targetPort"
|
|
222
|
-
]));
|
|
223
171
|
});
|
|
224
172
|
return issues;
|
|
225
173
|
}
|
|
@@ -16,6 +16,20 @@ interface EntityRect {
|
|
|
16
16
|
width: number;
|
|
17
17
|
height: number;
|
|
18
18
|
}
|
|
19
|
+
/** A 2D point, used for edge routing waypoints. */
|
|
20
|
+
interface Point {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* How an edge's {@link GraphEdge.points} should be interpreted by renderers:
|
|
26
|
+
*
|
|
27
|
+
* - `'polyline'` — straight segments through the points.
|
|
28
|
+
* - `'orthogonal'` — axis-aligned segments (ELK layered routing).
|
|
29
|
+
* - `'splines'` — bezier control points (Graphviz convention: 3n+1 chained
|
|
30
|
+
* cubic curves, tail → head).
|
|
31
|
+
*/
|
|
32
|
+
type EdgeRouting = 'polyline' | 'orthogonal' | 'splines';
|
|
19
33
|
/** Shared optional visual/style props for nodes, edges, ports. */
|
|
20
34
|
interface GraphEntity {
|
|
21
35
|
x?: number;
|
|
@@ -55,7 +69,7 @@ interface GraphConfig<TNodeData = any, TEdgeData = any, TGraphData = any, TPortD
|
|
|
55
69
|
id?: string;
|
|
56
70
|
/** Default directedness for all edges. Defaults to `'directed'`. */
|
|
57
71
|
mode?: GraphMode;
|
|
58
|
-
initialNodeId?: string;
|
|
72
|
+
initialNodeId?: string | null;
|
|
59
73
|
nodes?: NodeConfig<TNodeData, TPortData>[];
|
|
60
74
|
edges?: EdgeConfig<TEdgeData>[];
|
|
61
75
|
data?: TGraphData;
|
|
@@ -65,13 +79,20 @@ interface GraphConfig<TNodeData = any, TEdgeData = any, TGraphData = any, TPortD
|
|
|
65
79
|
interface NodeConfig<TNodeData = any, TPortData = any> extends GraphEntity {
|
|
66
80
|
id: string;
|
|
67
81
|
parentId?: string | null;
|
|
68
|
-
initialNodeId?: string;
|
|
82
|
+
initialNodeId?: string | null;
|
|
69
83
|
label?: string | null;
|
|
70
84
|
data?: TNodeData;
|
|
71
85
|
ports?: PortConfig<TPortData>[];
|
|
72
86
|
shape?: string;
|
|
73
87
|
color?: string;
|
|
74
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Note on edge geometry: an edge's `x`/`y`/`width`/`height` are canonically
|
|
91
|
+
* the **label rect** (top-left + size). Layout adapters write computed edge
|
|
92
|
+
* label positions here, and engines that need label dimensions as input
|
|
93
|
+
* (dagre, ELK) read `width`/`height`. The edge's *route* lives in
|
|
94
|
+
* {@link EdgeConfig.points}.
|
|
95
|
+
*/
|
|
75
96
|
interface EdgeConfig<TEdgeData = any> extends GraphEntity {
|
|
76
97
|
/**
|
|
77
98
|
* The id of the edge.
|
|
@@ -104,6 +125,13 @@ interface EdgeConfig<TEdgeData = any> extends GraphEntity {
|
|
|
104
125
|
* {@link GraphConfig.mode}.
|
|
105
126
|
*/
|
|
106
127
|
mode?: GraphMode;
|
|
128
|
+
/**
|
|
129
|
+
* Edge route waypoints (including endpoints, tail → head), as computed by a
|
|
130
|
+
* layout engine. Interpretation is governed by {@link EdgeConfig.routing}.
|
|
131
|
+
*/
|
|
132
|
+
points?: Point[];
|
|
133
|
+
/** How {@link EdgeConfig.points} should be interpreted. Default: polyline. */
|
|
134
|
+
routing?: EdgeRouting;
|
|
107
135
|
data?: TEdgeData;
|
|
108
136
|
color?: string;
|
|
109
137
|
}
|
|
@@ -129,6 +157,11 @@ interface GraphNode<TNodeData = any, TPortData = any> extends GraphEntity {
|
|
|
129
157
|
shape?: string;
|
|
130
158
|
color?: string;
|
|
131
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Note on edge geometry: an edge's `x`/`y`/`width`/`height` are canonically
|
|
162
|
+
* the **label rect** (top-left + size); the edge's *route* lives in
|
|
163
|
+
* {@link GraphEdge.points}. See {@link EdgeConfig} for details.
|
|
164
|
+
*/
|
|
132
165
|
interface GraphEdge<TEdgeData = any> extends GraphEntity {
|
|
133
166
|
type: 'edge';
|
|
134
167
|
id: string;
|
|
@@ -150,6 +183,13 @@ interface GraphEdge<TEdgeData = any> extends GraphEntity {
|
|
|
150
183
|
* {@link Graph.mode}.
|
|
151
184
|
*/
|
|
152
185
|
mode?: GraphMode;
|
|
186
|
+
/**
|
|
187
|
+
* Edge route waypoints (including endpoints, tail → head), as computed by a
|
|
188
|
+
* layout engine. Interpretation is governed by {@link GraphEdge.routing}.
|
|
189
|
+
*/
|
|
190
|
+
points?: Point[];
|
|
191
|
+
/** How {@link GraphEdge.points} should be interpreted. Default: polyline. */
|
|
192
|
+
routing?: EdgeRouting;
|
|
153
193
|
data: TEdgeData;
|
|
154
194
|
color?: string;
|
|
155
195
|
}
|
|
@@ -177,15 +217,67 @@ interface VisualGraphConfig<TNodeData = any, TEdgeData = any, TGraphData = any,
|
|
|
177
217
|
interface DeleteNodeOptions {
|
|
178
218
|
reparent?: boolean;
|
|
179
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Update payload for {@link updateNode}/`updateEntities`.
|
|
222
|
+
*
|
|
223
|
+
* Optional fields (`x`, `y`, `width`, `height`, `shape`, `color`, `style`,
|
|
224
|
+
* `ports`) accept `null` to **unset** the field. `undefined` (or omitting the
|
|
225
|
+
* key) leaves the field unchanged. `null` is used for unsetting so update
|
|
226
|
+
* payloads stay JSON-serializable.
|
|
227
|
+
*/
|
|
228
|
+
interface NodeUpdate<TNodeData = any, TPortData = any> {
|
|
229
|
+
parentId?: string | null;
|
|
230
|
+
initialNodeId?: string | null;
|
|
231
|
+
label?: string | null;
|
|
232
|
+
data?: TNodeData;
|
|
233
|
+
/** New ports for the node, or `null` to remove all ports. */
|
|
234
|
+
ports?: PortConfig<TPortData>[] | null;
|
|
235
|
+
x?: number | null;
|
|
236
|
+
y?: number | null;
|
|
237
|
+
width?: number | null;
|
|
238
|
+
height?: number | null;
|
|
239
|
+
shape?: string | null;
|
|
240
|
+
color?: string | null;
|
|
241
|
+
style?: Record<string, string | number | boolean> | null;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Update payload for {@link updateEdge}/`updateEntities`.
|
|
245
|
+
*
|
|
246
|
+
* Optional fields (`weight`, `mode`, `sourcePort`, `targetPort`, `x`, `y`,
|
|
247
|
+
* `width`, `height`, `color`, `style`) accept `null` to **unset** the field.
|
|
248
|
+
* `undefined` (or omitting the key) leaves the field unchanged. `null` is
|
|
249
|
+
* used for unsetting so update payloads stay JSON-serializable.
|
|
250
|
+
*/
|
|
251
|
+
interface EdgeUpdate<TEdgeData = any> {
|
|
252
|
+
sourceId?: string;
|
|
253
|
+
targetId?: string;
|
|
254
|
+
label?: string | null;
|
|
255
|
+
data?: TEdgeData;
|
|
256
|
+
weight?: number | null;
|
|
257
|
+
mode?: GraphMode | null;
|
|
258
|
+
/** Port name on the source node, or `null` to clear the port reference. */
|
|
259
|
+
sourcePort?: string | null;
|
|
260
|
+
/** Port name on the target node, or `null` to clear the port reference. */
|
|
261
|
+
targetPort?: string | null;
|
|
262
|
+
/** Edge route waypoints, or `null` to clear the route. */
|
|
263
|
+
points?: Point[] | null;
|
|
264
|
+
routing?: EdgeRouting | null;
|
|
265
|
+
x?: number | null;
|
|
266
|
+
y?: number | null;
|
|
267
|
+
width?: number | null;
|
|
268
|
+
height?: number | null;
|
|
269
|
+
color?: string | null;
|
|
270
|
+
style?: Record<string, string | number | boolean> | null;
|
|
271
|
+
}
|
|
180
272
|
interface EntitiesConfig<TNodeData = any, TEdgeData = any, TPortData = any> {
|
|
181
273
|
nodes?: NodeConfig<TNodeData, TPortData>[];
|
|
182
274
|
edges?: EdgeConfig<TEdgeData>[];
|
|
183
275
|
}
|
|
184
276
|
interface EntitiesUpdate<TNodeData = any, TEdgeData = any, TPortData = any> {
|
|
185
|
-
nodes?: (
|
|
277
|
+
nodes?: (NodeUpdate<TNodeData, TPortData> & {
|
|
186
278
|
id: string;
|
|
187
279
|
})[];
|
|
188
|
-
edges?: (
|
|
280
|
+
edges?: (EdgeUpdate<TEdgeData> & {
|
|
189
281
|
id: string;
|
|
190
282
|
})[];
|
|
191
283
|
}
|
|
@@ -281,7 +373,7 @@ type GraphPatch<TNodeData = any, TEdgeData = any> = {
|
|
|
281
373
|
} | {
|
|
282
374
|
op: 'updateNode';
|
|
283
375
|
id: string;
|
|
284
|
-
data:
|
|
376
|
+
data: NodeUpdate<TNodeData>;
|
|
285
377
|
description?: string;
|
|
286
378
|
} | {
|
|
287
379
|
op: 'deleteNode';
|
|
@@ -294,7 +386,7 @@ type GraphPatch<TNodeData = any, TEdgeData = any> = {
|
|
|
294
386
|
} | {
|
|
295
387
|
op: 'updateEdge';
|
|
296
388
|
id: string;
|
|
297
|
-
data:
|
|
389
|
+
data: EdgeUpdate<TEdgeData>;
|
|
298
390
|
description?: string;
|
|
299
391
|
} | {
|
|
300
392
|
op: 'deleteEdge';
|
|
@@ -375,4 +467,4 @@ interface TransitionOptions<TState, TEvent> {
|
|
|
375
467
|
id?: string;
|
|
376
468
|
}
|
|
377
469
|
//#endregion
|
|
378
|
-
export {
|
|
470
|
+
export { PortConfig as A, VisualNode as B, GraphStep as C, NodeUpdate as D, NodeConfig as E, VisualEdge as F, WalkContext as H, VisualGraph as I, VisualGraphConfig as L, SinglePathOptions as M, TransitionOptions as N, PathOptions as O, TraversalOptions as P, VisualGraphEntity as R, GraphPort as S, NodeChange as T, WalkOptions as U, VisualPort as V, WeightedWalkOptions as W, GraphFormatConverter as _, EdgeChange as a, GraphPatch as b, EdgeUpdate as c, EntityRect as d, Graph as f, GraphEntity as g, GraphEdge as h, DeleteNodeOptions as i, PortDirection as j, Point as k, EntitiesConfig as l, GraphDiff as m, AllPairsShortestPathsOptions as n, EdgeConfig as o, GraphConfig as p, CoverageStats as r, EdgeRouting as s, AStarOptions as t, EntitiesUpdate as u, GraphMode as v, MSTOptions as w, GraphPath as x, GraphNode as y, VisualGraphFormatConverter as z };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
//#region src/validate.ts
|
|
2
|
+
function getDuplicateIndexes(items, getKey) {
|
|
3
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
4
|
+
items.forEach((item, index) => {
|
|
5
|
+
const key = getKey(item);
|
|
6
|
+
if (key == null) return;
|
|
7
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
8
|
+
indexes.push(index);
|
|
9
|
+
indexesByKey.set(key, indexes);
|
|
10
|
+
});
|
|
11
|
+
for (const [key, indexes] of indexesByKey) if (indexes.length < 2) indexesByKey.delete(key);
|
|
12
|
+
return indexesByKey;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns the parent cycles in a graph's hierarchy, each cycle reported once
|
|
16
|
+
* as the list of node ids forming the cycle (cycle members only — nodes whose
|
|
17
|
+
* ancestry merely *leads into* a cycle are not included).
|
|
18
|
+
*/
|
|
19
|
+
function getParentCycles(nodesById) {
|
|
20
|
+
const cycles = [];
|
|
21
|
+
const visited = /* @__PURE__ */ new Set();
|
|
22
|
+
for (const startId of nodesById.keys()) {
|
|
23
|
+
if (visited.has(startId)) continue;
|
|
24
|
+
const path = [];
|
|
25
|
+
const positions = /* @__PURE__ */ new Map();
|
|
26
|
+
let currentId = startId;
|
|
27
|
+
while (currentId != null && !visited.has(currentId) && nodesById.has(currentId)) {
|
|
28
|
+
const position = positions.get(currentId);
|
|
29
|
+
if (position !== void 0) {
|
|
30
|
+
cycles.push(path.slice(position));
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
positions.set(currentId, path.length);
|
|
34
|
+
path.push(currentId);
|
|
35
|
+
currentId = nodesById.get(currentId).parentId;
|
|
36
|
+
}
|
|
37
|
+
for (const id of path) visited.add(id);
|
|
38
|
+
}
|
|
39
|
+
return cycles;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validates the structural invariants of a graph and returns the issues
|
|
43
|
+
* found, or `[]` when the graph is valid. Pure — never throws, never mutates.
|
|
44
|
+
*
|
|
45
|
+
* This is the recommended gate for untrusted or imported graphs (e.g. parsed
|
|
46
|
+
* from a file or received over the wire) before handing them to queries and
|
|
47
|
+
* algorithms: the mutation APIs (`addNode`, `addEdge`, `updateNode`, …)
|
|
48
|
+
* validate incrementally, but `createGraph` does **not** — it accepts
|
|
49
|
+
* dangling `parentId`/edge references and even `parentId` cycles as-is.
|
|
50
|
+
*
|
|
51
|
+
* For Zod-based shape validation of arbitrary unknown values, see
|
|
52
|
+
* `validateGraph` in `@statelyai/graph/schemas` (which reuses these checks).
|
|
53
|
+
*
|
|
54
|
+
* Issue codes: `duplicate-node-id`, `duplicate-edge-id`,
|
|
55
|
+
* `missing-initial-node`, `missing-parent`, `missing-node-initial`,
|
|
56
|
+
* `duplicate-port-name`, `parent-cycle`, `dangling-edge-endpoint`,
|
|
57
|
+
* `missing-source-port`, `missing-target-port`.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const graph = createGraph({
|
|
62
|
+
* nodes: [{ id: 'a', parentId: 'ghost' }],
|
|
63
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
64
|
+
* });
|
|
65
|
+
* getGraphIssues(graph);
|
|
66
|
+
* // => [
|
|
67
|
+
* // { code: 'missing-parent', message: '...', path: ['nodes', 0, 'parentId'] },
|
|
68
|
+
* // { code: 'dangling-edge-endpoint', message: '...', path: ['edges', 0, 'targetId'] },
|
|
69
|
+
* // ]
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
function getGraphIssues(graph) {
|
|
73
|
+
const issues = [];
|
|
74
|
+
const nodeIndexes = /* @__PURE__ */ new Map();
|
|
75
|
+
const nodesById = /* @__PURE__ */ new Map();
|
|
76
|
+
graph.nodes.forEach((node, index) => {
|
|
77
|
+
nodeIndexes.set(node.id, index);
|
|
78
|
+
nodesById.set(node.id, node);
|
|
79
|
+
});
|
|
80
|
+
for (const [id, indexes] of getDuplicateIndexes(graph.nodes, (node) => node.id)) for (const index of indexes) issues.push({
|
|
81
|
+
code: "duplicate-node-id",
|
|
82
|
+
message: `Duplicate node id "${id}". Node ids must be unique; rename or remove the duplicates.`,
|
|
83
|
+
path: [
|
|
84
|
+
"nodes",
|
|
85
|
+
index,
|
|
86
|
+
"id"
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
for (const [id, indexes] of getDuplicateIndexes(graph.edges, (edge) => edge.id)) for (const index of indexes) issues.push({
|
|
90
|
+
code: "duplicate-edge-id",
|
|
91
|
+
message: `Duplicate edge id "${id}". Edge ids must be unique; rename or remove the duplicates.`,
|
|
92
|
+
path: [
|
|
93
|
+
"edges",
|
|
94
|
+
index,
|
|
95
|
+
"id"
|
|
96
|
+
]
|
|
97
|
+
});
|
|
98
|
+
if (graph.initialNodeId && !nodesById.has(graph.initialNodeId)) issues.push({
|
|
99
|
+
code: "missing-initial-node",
|
|
100
|
+
message: `Graph initialNodeId references missing node "${graph.initialNodeId}". Add that node or update the graph's initialNodeId.`,
|
|
101
|
+
path: ["initialNodeId"]
|
|
102
|
+
});
|
|
103
|
+
graph.nodes.forEach((node, index) => {
|
|
104
|
+
if (node.parentId != null && !nodesById.has(node.parentId)) issues.push({
|
|
105
|
+
code: "missing-parent",
|
|
106
|
+
message: `Node "${node.id}" has parentId "${node.parentId}", which does not exist. Add that node or remove the parentId.`,
|
|
107
|
+
path: [
|
|
108
|
+
"nodes",
|
|
109
|
+
index,
|
|
110
|
+
"parentId"
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
if (node.initialNodeId && !nodesById.has(node.initialNodeId)) issues.push({
|
|
114
|
+
code: "missing-node-initial",
|
|
115
|
+
message: `Node "${node.id}" has initialNodeId "${node.initialNodeId}", which does not exist. Add that node or remove the initialNodeId.`,
|
|
116
|
+
path: [
|
|
117
|
+
"nodes",
|
|
118
|
+
index,
|
|
119
|
+
"initialNodeId"
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
for (const [name, portIndexes] of getDuplicateIndexes(node.ports ?? [], (port) => port.name)) for (const portIndex of portIndexes) issues.push({
|
|
123
|
+
code: "duplicate-port-name",
|
|
124
|
+
message: `Duplicate port name "${name}" on node "${node.id}". Port names must be unique per node; rename or remove the duplicates.`,
|
|
125
|
+
path: [
|
|
126
|
+
"nodes",
|
|
127
|
+
index,
|
|
128
|
+
"ports",
|
|
129
|
+
portIndex,
|
|
130
|
+
"name"
|
|
131
|
+
]
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
for (const cycle of getParentCycles(nodesById)) {
|
|
135
|
+
const chain = [...cycle, cycle[0]].join(" → ");
|
|
136
|
+
issues.push({
|
|
137
|
+
code: "parent-cycle",
|
|
138
|
+
message: `Parent cycle detected: ${chain}. Break the cycle by changing the parentId of one of these nodes.`,
|
|
139
|
+
path: [
|
|
140
|
+
"nodes",
|
|
141
|
+
nodeIndexes.get(cycle[0]) ?? 0,
|
|
142
|
+
"parentId"
|
|
143
|
+
]
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
graph.edges.forEach((edge, index) => {
|
|
147
|
+
const source = nodesById.get(edge.sourceId);
|
|
148
|
+
const target = nodesById.get(edge.targetId);
|
|
149
|
+
if (!source) issues.push({
|
|
150
|
+
code: "dangling-edge-endpoint",
|
|
151
|
+
message: `Edge "${edge.id}" has sourceId "${edge.sourceId}", which references a missing node. Add that node or fix the edge's sourceId.`,
|
|
152
|
+
path: [
|
|
153
|
+
"edges",
|
|
154
|
+
index,
|
|
155
|
+
"sourceId"
|
|
156
|
+
]
|
|
157
|
+
});
|
|
158
|
+
if (!target) issues.push({
|
|
159
|
+
code: "dangling-edge-endpoint",
|
|
160
|
+
message: `Edge "${edge.id}" has targetId "${edge.targetId}", which references a missing node. Add that node or fix the edge's targetId.`,
|
|
161
|
+
path: [
|
|
162
|
+
"edges",
|
|
163
|
+
index,
|
|
164
|
+
"targetId"
|
|
165
|
+
]
|
|
166
|
+
});
|
|
167
|
+
if (source && edge.sourcePort !== void 0 && !source.ports?.some((port) => port.name === edge.sourcePort)) issues.push({
|
|
168
|
+
code: "missing-source-port",
|
|
169
|
+
message: `Edge "${edge.id}" has sourcePort "${edge.sourcePort}", but source node "${edge.sourceId}" has no port with that name. Add the port or fix the edge's sourcePort.`,
|
|
170
|
+
path: [
|
|
171
|
+
"edges",
|
|
172
|
+
index,
|
|
173
|
+
"sourcePort"
|
|
174
|
+
]
|
|
175
|
+
});
|
|
176
|
+
if (target && edge.targetPort !== void 0 && !target.ports?.some((port) => port.name === edge.targetPort)) issues.push({
|
|
177
|
+
code: "missing-target-port",
|
|
178
|
+
message: `Edge "${edge.id}" has targetPort "${edge.targetPort}", but target node "${edge.targetId}" has no port with that name. Add the port or fix the edge's targetPort.`,
|
|
179
|
+
path: [
|
|
180
|
+
"edges",
|
|
181
|
+
index,
|
|
182
|
+
"targetPort"
|
|
183
|
+
]
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
return issues;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
//#endregion
|
|
190
|
+
export { getGraphIssues as t };
|