@statelyai/graph 0.11.1 → 0.12.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 CHANGED
@@ -1,7 +1,3 @@
1
- ---
2
- title: '@statelyai/graph'
3
- ---
4
-
5
1
  # @statelyai/graph
6
2
 
7
3
  A TypeScript graph library built on plain JSON objects. Supports directed/undirected graphs, hierarchical nodes, graph algorithms, visual properties, and serialization to DOT, GraphML, Mermaid, and more.
@@ -18,14 +14,14 @@ Optional peers are only needed for specific adapters:
18
14
 
19
15
  <!-- optional peer dependencies derived from package.json#peerDependencies -->
20
16
 
21
- | Package | Needed for |
22
- | --- | --- |
17
+ | Package | Needed for |
18
+ | ----------------- | --------------------------------------------------- |
23
19
  | `fast-xml-parser` | `@statelyai/graph/gexf`, `@statelyai/graph/graphml` |
24
- | `dotparser` | `@statelyai/graph/dot` parsing |
25
- | `cytoscape` | Cytoscape integration tests and consumer typing |
26
- | `d3-force` | D3 force integration tests and consumer typing |
27
- | `elkjs` | `@statelyai/graph/elk` |
28
- | `zod` | `@statelyai/graph/schemas` |
20
+ | `dotparser` | `@statelyai/graph/dot` parsing |
21
+ | `cytoscape` | Cytoscape integration tests and consumer typing |
22
+ | `d3-force` | D3 force integration tests and consumer typing |
23
+ | `elkjs` | `@statelyai/graph/elk` |
24
+ | `zod` | `@statelyai/graph/schemas` |
29
25
 
30
26
  ## Highlights
31
27
 
@@ -43,7 +39,12 @@ Optional peers are only needed for specific adapters:
43
39
  Graphs are plain JSON-serializable objects. All operations are standalone functions — no classes, no DOM, no rendering engine.
44
40
 
45
41
  ```ts
46
- import { createGraph, addNode, addEdge, getShortestPath } from '@statelyai/graph';
42
+ import {
43
+ createGraph,
44
+ addNode,
45
+ addEdge,
46
+ getShortestPath,
47
+ } from '@statelyai/graph';
47
48
 
48
49
  const graph = createGraph({
49
50
  nodes: [
@@ -70,12 +71,17 @@ const path = getShortestPath(graph, { from: 'a', to: 'c' });
70
71
  Look up, add, delete, and update nodes and edges. Query neighbors, predecessors, successors, degree, and more.
71
72
 
72
73
  ```ts
73
- import { getNode, deleteNode, getNeighbors, getSources } from '@statelyai/graph';
74
+ import {
75
+ getNode,
76
+ deleteNode,
77
+ getNeighbors,
78
+ getSources,
79
+ } from '@statelyai/graph';
74
80
 
75
- const node = getNode(graph, 'a'); // lookup by id
76
- deleteNode(graph, 'd'); // removes node + connected edges
81
+ const node = getNode(graph, 'a'); // lookup by id
82
+ deleteNode(graph, 'd'); // removes node + connected edges
77
83
  const neighbors = getNeighbors(graph, 'a'); // adjacent nodes
78
- const roots = getSources(graph); // nodes with no incoming edges
84
+ const roots = getSources(graph); // nodes with no incoming edges
79
85
  ```
80
86
 
81
87
  Batch operations (`addEntities`, `deleteEntities`, `updateEntities`) let you apply multiple changes at once.
@@ -96,14 +102,14 @@ const graph = createGraph({
96
102
  { id: 'c' },
97
103
  ],
98
104
  edges: [
99
- { id: 'e1', sourceId: 'a', targetId: 'b' }, // resolves to a -> b1
105
+ { id: 'e1', sourceId: 'a', targetId: 'b' }, // resolves to a -> b1
100
106
  { id: 'e2', sourceId: 'b1', targetId: 'b2' },
101
- { id: 'e3', sourceId: 'b', targetId: 'c' }, // expands from all leaves of b
107
+ { id: 'e3', sourceId: 'b', targetId: 'c' }, // expands from all leaves of b
102
108
  ],
103
109
  });
104
110
 
105
111
  const children = getChildren(graph, 'b'); // [b1, b2]
106
- const flat = flatten(graph); // only leaf nodes, edges resolved
112
+ const flat = flatten(graph); // only leaf nodes, edges resolved
107
113
  ```
108
114
 
109
115
  ## Ports
@@ -139,6 +145,26 @@ getPorts(graph, 'fetch'); // [{ name: 'result', ... }]
139
145
  getEdgesByPort(graph, 'render', 'input'); // [e1]
140
146
  ```
141
147
 
148
+ ## Schema Validation
149
+
150
+ <!-- validation helpers exported from src/schemas.ts -->
151
+
152
+ Use the `@statelyai/graph/schemas` subpath when you want runtime validation or JSON Schema generation.
153
+
154
+ ```ts
155
+ import { GraphSchema, getGraphIssues, isGraph } from '@statelyai/graph/schemas';
156
+
157
+ const unknownValue: unknown = JSON.parse(input);
158
+
159
+ if (isGraph(unknownValue)) {
160
+ // fully typed Graph
161
+ } else {
162
+ console.error(getGraphIssues(unknownValue));
163
+ }
164
+
165
+ const parsed = GraphSchema.parse(unknownValue);
166
+ ```
167
+
142
168
  ## Algorithms
143
169
 
144
170
  <!-- algorithm functions exported from src/algorithms.ts -->
@@ -147,27 +173,40 @@ Includes traversal (BFS, DFS, preorder/postorder), pathfinding (shortest path, s
147
173
 
148
174
  ```ts
149
175
  import {
150
- bfs, dfs, hasPath, isAcyclic,
151
- getShortestPath, getCycles, getTopologicalSort,
152
- getConnectedComponents, getMinimumSpanningTree,
153
- getPageRank, getLabelPropagationCommunities,
154
- genGirvanNewmanCommunities, getBridges, isIsomorphic,
176
+ bfs,
177
+ dfs,
178
+ hasPath,
179
+ isAcyclic,
180
+ getShortestPath,
181
+ getCycles,
182
+ getTopologicalSort,
183
+ getConnectedComponents,
184
+ getMinimumSpanningTree,
185
+ getPageRank,
186
+ getLabelPropagationCommunities,
187
+ genGirvanNewmanCommunities,
188
+ getBridges,
189
+ isIsomorphic,
155
190
  } from '@statelyai/graph';
156
191
 
157
- for (const node of bfs(graph, 'a')) { /* breadth-first */ }
158
- for (const node of dfs(graph, 'a')) { /* depth-first */ }
159
-
160
- hasPath(graph, 'a', 'c'); // reachability
161
- isAcyclic(graph); // cycle check
162
- getShortestPath(graph, { from: 'a', to: 'c' }); // single shortest path
163
- getTopologicalSort(graph); // topological order (or null)
164
- getConnectedComponents(graph); // connected components
165
- getMinimumSpanningTree(graph, { weight: e => e.data?.weight ?? 1 }); // MST
166
- getPageRank(graph); // link analysis scores
167
- getLabelPropagationCommunities(graph); // community detection
168
- [...genGirvanNewmanCommunities(graph)]; // lazy community splits
169
- getBridges(graph); // bridge edges
170
- isIsomorphic(graph, otherGraph); // structural equivalence
192
+ for (const node of bfs(graph, 'a')) {
193
+ /* breadth-first */
194
+ }
195
+ for (const node of dfs(graph, 'a')) {
196
+ /* depth-first */
197
+ }
198
+
199
+ hasPath(graph, 'a', 'c'); // reachability
200
+ isAcyclic(graph); // cycle check
201
+ getShortestPath(graph, { from: 'a', to: 'c' }); // single shortest path
202
+ getTopologicalSort(graph); // topological order (or null)
203
+ getConnectedComponents(graph); // connected components
204
+ getMinimumSpanningTree(graph, { weight: (e) => e.data?.weight ?? 1 }); // MST
205
+ getPageRank(graph); // link analysis scores
206
+ getLabelPropagationCommunities(graph); // community detection
207
+ [...genGirvanNewmanCommunities(graph)]; // lazy community splits
208
+ getBridges(graph); // bridge edges
209
+ isIsomorphic(graph, otherGraph); // structural equivalence
171
210
  ```
172
211
 
173
212
  ## Diff & Walks
@@ -205,10 +244,10 @@ import { fromGEXF } from '@statelyai/graph/gexf';
205
244
  import { toCytoscapeJSON } from '@statelyai/graph/cytoscape';
206
245
  import { toD3Graph } from '@statelyai/graph/d3';
207
246
 
208
- const dot = toDOT(graph); // Graphviz DOT
209
- const cytoData = toCytoscapeJSON(graph); // Cytoscape.js JSON
210
- const d3Data = toD3Graph(graph); // D3.js { nodes, links }
211
- const imported = fromGEXF(gexfXmlString); // GEXF (Gephi)
247
+ const dot = toDOT(graph); // Graphviz DOT
248
+ const cytoData = toCytoscapeJSON(graph); // Cytoscape.js JSON
249
+ const d3Data = toD3Graph(graph); // D3.js { nodes, links }
250
+ const imported = fromGEXF(gexfXmlString); // GEXF (Gephi)
212
251
  ```
213
252
 
214
253
  <!-- supported format adapters derived from src/formats/* subdirectories -->
@@ -224,6 +263,33 @@ const cyto = cytoscapeConverter.to(graph);
224
263
  const back = cytoscapeConverter.from(cyto);
225
264
  ```
226
265
 
266
+ ## Format Support
267
+
268
+ <!-- format support matrix derived from src/formats/support.ts -->
269
+
270
+ | Format | Hierarchy | Ports | Visual | Round-trip | Notes |
271
+ | ------------------- | --------- | ------- | ------- | ---------- | -------------------------------------------------------------------------- |
272
+ | `adjacency-list` | none | none | none | partial | Connectivity only; edge metadata is lost. |
273
+ | `cytoscape` | full | full | partial | partial | Ports round-trip through element data. |
274
+ | `d3` | none | full | partial | partial | Ports round-trip through node/link objects. |
275
+ | `dot` | partial | partial | partial | partial | Edge port ids round-trip, but `:port:compass` mapping is still incomplete. |
276
+ | `edge-list` | none | none | none | partial | Endpoints only. |
277
+ | `elk` | full | full | full | partial | Best for layout exchange. |
278
+ | `gexf` | full | full | partial | partial | Ports round-trip via custom attributes. |
279
+ | `gml` | full | full | partial | partial | Ports round-trip through JSON-stringified metadata. |
280
+ | `graphml` | full | full | partial | partial | Ports round-trip through `<data>` fields. |
281
+ | `jgf` | full | full | none | partial | Ports round-trip through metadata. |
282
+ | `tgf` | none | none | none | partial | Minimal ids and labels only. |
283
+ | `xyflow` | none | full | full | partial | Ports map directly to handles. |
284
+ | `mermaid/block` | partial | none | partial | partial | Syntax-driven, not port-aware. |
285
+ | `mermaid/class` | none | none | none | partial | Class syntax is stored conservatively. |
286
+ | `mermaid/er` | none | none | none | partial | Focuses on entities and cardinality. |
287
+ | `mermaid/flowchart` | partial | none | partial | partial | `linkStyle` indices are fragile. |
288
+ | `mermaid/ishikawa` | full | none | none | partial | Preserves hierarchy, not fishbone layout. |
289
+ | `mermaid/mindmap` | full | none | partial | partial | Icon syntax is not fully re-emitted. |
290
+ | `mermaid/sequence` | partial | none | none | partial | Actor links and menu syntax are incomplete. |
291
+ | `mermaid/state` | full | none | partial | partial | State notes are still lossy. |
292
+
227
293
  Some formats have optional peer dependencies: `fast-xml-parser` (GEXF, GraphML) and `dotparser` (DOT). All other formats are dependency-free.
228
294
 
229
295
  Format-specific docs live alongside the source:
@@ -21,14 +21,14 @@ const FORMAT_SUPPORT_MATRIX = [
21
21
  features: {
22
22
  directed: "full",
23
23
  undirected: "full",
24
- hierarchy: "partial",
25
- ports: "none",
24
+ hierarchy: "full",
25
+ ports: "full",
26
26
  visual: "partial",
27
27
  style: "partial",
28
28
  weight: "full",
29
29
  roundTrip: "partial"
30
30
  },
31
- notes: ["Uses Cytoscape JSON element data with partial layout/style fidelity."]
31
+ notes: ["Uses Cytoscape JSON element data with partial layout/style fidelity.", "Ports round-trip through element data as `ports`, `sourcePort`, and `targetPort`."]
32
32
  },
33
33
  {
34
34
  id: "d3",
@@ -37,13 +37,13 @@ const FORMAT_SUPPORT_MATRIX = [
37
37
  directed: "full",
38
38
  undirected: "full",
39
39
  hierarchy: "none",
40
- ports: "none",
40
+ ports: "full",
41
41
  visual: "partial",
42
42
  style: "none",
43
43
  weight: "full",
44
44
  roundTrip: "partial"
45
45
  },
46
- notes: ["Targets force-graph structures, not compound graph metadata."]
46
+ notes: ["Targets force-graph structures, not compound graph metadata.", "Ports round-trip through node/link objects."]
47
47
  },
48
48
  {
49
49
  id: "dot",
@@ -58,7 +58,7 @@ const FORMAT_SUPPORT_MATRIX = [
58
58
  weight: "none",
59
59
  roundTrip: "partial"
60
60
  },
61
- notes: ["Port syntax (`:port:compass`) is not fully mapped.", "HTML labels and layout hints beyond `rankdir` are lossy."]
61
+ notes: ["Edge port ids round-trip, but compass points and node port definitions are not mapped.", "HTML labels and layout hints beyond `rankdir` are lossy."]
62
62
  },
63
63
  {
64
64
  id: "edge-list",
@@ -96,14 +96,14 @@ const FORMAT_SUPPORT_MATRIX = [
96
96
  features: {
97
97
  directed: "full",
98
98
  undirected: "full",
99
- hierarchy: "none",
100
- ports: "none",
99
+ hierarchy: "full",
100
+ ports: "full",
101
101
  visual: "partial",
102
102
  style: "partial",
103
103
  weight: "full",
104
104
  roundTrip: "partial"
105
105
  },
106
- notes: ["Attribute and viz extensions round-trip partially."]
106
+ notes: ["Attribute and viz extensions round-trip partially.", "Ports round-trip via custom node/edge attributes."]
107
107
  },
108
108
  {
109
109
  id: "gml",
@@ -111,14 +111,14 @@ const FORMAT_SUPPORT_MATRIX = [
111
111
  features: {
112
112
  directed: "full",
113
113
  undirected: "full",
114
- hierarchy: "none",
115
- ports: "none",
114
+ hierarchy: "full",
115
+ ports: "full",
116
116
  visual: "partial",
117
117
  style: "partial",
118
118
  weight: "full",
119
119
  roundTrip: "partial"
120
120
  },
121
- notes: ["GML support focuses on common graph/node/edge attributes."]
121
+ notes: ["GML support focuses on common graph/node/edge attributes.", "Ports round-trip through JSON-stringified node metadata and edge fields."]
122
122
  },
123
123
  {
124
124
  id: "graphml",
@@ -126,14 +126,14 @@ const FORMAT_SUPPORT_MATRIX = [
126
126
  features: {
127
127
  directed: "full",
128
128
  undirected: "full",
129
- hierarchy: "none",
130
- ports: "none",
129
+ hierarchy: "full",
130
+ ports: "full",
131
131
  visual: "partial",
132
132
  style: "partial",
133
133
  weight: "full",
134
134
  roundTrip: "partial"
135
135
  },
136
- notes: ["GraphML attribute fidelity is good, but not every extension is represented."]
136
+ notes: ["GraphML attribute fidelity is good, but not every extension is represented.", "Ports round-trip through node and edge `<data>` fields."]
137
137
  },
138
138
  {
139
139
  id: "jgf",
@@ -141,14 +141,14 @@ const FORMAT_SUPPORT_MATRIX = [
141
141
  features: {
142
142
  directed: "full",
143
143
  undirected: "full",
144
- hierarchy: "none",
145
- ports: "none",
144
+ hierarchy: "full",
145
+ ports: "full",
146
146
  visual: "none",
147
147
  style: "none",
148
148
  weight: "full",
149
149
  roundTrip: "partial"
150
150
  },
151
- notes: ["JGF preserves core graph structure and data, but not layout primitives."]
151
+ notes: ["JGF preserves core graph structure and data, but not layout primitives.", "Ports round-trip through node and edge metadata."]
152
152
  },
153
153
  {
154
154
  id: "tgf",
@@ -38,6 +38,7 @@ function toCytoscapeJSON(graph) {
38
38
  if (n.height !== void 0) data.height = n.height;
39
39
  if (n.shape) data.shape = n.shape;
40
40
  if (n.color) data.color = n.color;
41
+ if (n.ports !== void 0) data.ports = n.ports;
41
42
  const node = { data };
42
43
  if (n.x !== void 0 && n.y !== void 0) node.position = {
43
44
  x: n.x,
@@ -54,6 +55,8 @@ function toCytoscapeJSON(graph) {
54
55
  if (e.label) data.label = e.label;
55
56
  if (e.data !== void 0) data.edgeData = e.data;
56
57
  if (e.color) data.color = e.color;
58
+ if (e.sourcePort !== void 0) data.sourcePort = e.sourcePort;
59
+ if (e.targetPort !== void 0) data.targetPort = e.targetPort;
57
60
  return { data };
58
61
  })
59
62
  }
@@ -99,7 +102,8 @@ function fromCytoscapeJSON(cyto) {
99
102
  ...n.data.width !== void 0 && { width: n.data.width },
100
103
  ...n.data.height !== void 0 && { height: n.data.height },
101
104
  ...n.data.shape && { shape: n.data.shape },
102
- ...n.data.color && { color: n.data.color }
105
+ ...n.data.color && { color: n.data.color },
106
+ ...n.data.ports !== void 0 && { ports: n.data.ports }
103
107
  })),
104
108
  edges: cyto.elements.edges.map((e, i) => ({
105
109
  type: "edge",
@@ -108,7 +112,9 @@ function fromCytoscapeJSON(cyto) {
108
112
  targetId: e.data.target,
109
113
  label: e.data.label ?? "",
110
114
  data: e.data.edgeData,
111
- ...e.data.color && { color: e.data.color }
115
+ ...e.data.color && { color: e.data.color },
116
+ ...e.data.sourcePort !== void 0 && { sourcePort: e.data.sourcePort },
117
+ ...e.data.targetPort !== void 0 && { targetPort: e.data.targetPort }
112
118
  }))
113
119
  };
114
120
  }
@@ -28,6 +28,7 @@ function toD3Graph(graph) {
28
28
  if (n.y !== void 0) node.y = n.y;
29
29
  if (n.color) node.color = n.color;
30
30
  if (n.shape) node.shape = n.shape;
31
+ if (n.ports !== void 0) node.ports = n.ports;
31
32
  return node;
32
33
  }),
33
34
  links: graph.edges.map((e) => {
@@ -39,6 +40,8 @@ function toD3Graph(graph) {
39
40
  if (e.label) link.label = e.label;
40
41
  if (e.data !== void 0) link.data = e.data;
41
42
  if (e.color) link.color = e.color;
43
+ if (e.sourcePort !== void 0) link.sourcePort = e.sourcePort;
44
+ if (e.targetPort !== void 0) link.targetPort = e.targetPort;
42
45
  return link;
43
46
  })
44
47
  };
@@ -75,7 +78,8 @@ function fromD3Graph(d3) {
75
78
  ...n.x !== void 0 && { x: n.x },
76
79
  ...n.y !== void 0 && { y: n.y },
77
80
  ...n.color && { color: n.color },
78
- ...n.shape && { shape: n.shape }
81
+ ...n.shape && { shape: n.shape },
82
+ ...n.ports !== void 0 && { ports: n.ports }
79
83
  })),
80
84
  edges: d3.links.map((l, i) => ({
81
85
  type: "edge",
@@ -84,7 +88,9 @@ function fromD3Graph(d3) {
84
88
  targetId: typeof l.target === "string" ? l.target : l.target.id,
85
89
  label: l.label ?? "",
86
90
  data: l.data,
87
- ...l.color && { color: l.color }
91
+ ...l.color && { color: l.color },
92
+ ...l.sourcePort !== void 0 && { sourcePort: l.sourcePort },
93
+ ...l.targetPort !== void 0 && { targetPort: l.targetPort }
88
94
  }))
89
95
  };
90
96
  }
@@ -11,6 +11,9 @@ function escapeId(id) {
11
11
  function escapeLabel(label) {
12
12
  return label.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
13
13
  }
14
+ function formatEndpoint(id, port) {
15
+ return `${escapeId(id)}${port ? `:${escapeId(port)}` : ""}`;
16
+ }
14
17
  const DIRECTION_TO_RANKDIR = {
15
18
  down: "TB",
16
19
  up: "BT",
@@ -72,7 +75,7 @@ function toDOT(graph) {
72
75
  if (edge.label) attrs.push(`label="${escapeLabel(edge.label)}"`);
73
76
  if (edge.color) attrs.push(`color="${escapeLabel(edge.color)}"`);
74
77
  const attrStr = attrs.length > 0 ? ` [${attrs.join(", ")}]` : "";
75
- lines.push(` ${escapeId(edge.sourceId)} ${edgeOp} ${escapeId(edge.targetId)}${attrStr};`);
78
+ lines.push(` ${formatEndpoint(edge.sourceId, edge.sourcePort)} ${edgeOp} ${formatEndpoint(edge.targetId, edge.targetPort)}${attrStr};`);
76
79
  }
77
80
  lines.push("}");
78
81
  return lines.join("\n");
@@ -95,6 +98,10 @@ const DOT_TO_SHAPE = {
95
98
  cylinder: "cylinder",
96
99
  parallelogram: "parallelogram"
97
100
  };
101
+ function getPortId(nodeId) {
102
+ const port = nodeId.port;
103
+ return typeof port?.id === "string" ? port.id : void 0;
104
+ }
98
105
  function attrsToMap(attrList) {
99
106
  const map = {};
100
107
  for (const a of attrList) map[a.id] = String(a.eq);
@@ -209,24 +216,29 @@ function fromDOT(dot) {
209
216
  const endpointGroups = [];
210
217
  for (const item of stmt.edge_list) if (item.type === "node_id") {
211
218
  ensureNode(item.id, parentId, nd);
212
- endpointGroups.push([item.id]);
219
+ endpointGroups.push([{
220
+ id: item.id,
221
+ ...getPortId(item) && { port: getPortId(item) }
222
+ }]);
213
223
  } else if (item.type === "subgraph") {
214
224
  walkChildren(item.children, parentId, nd, ed);
215
225
  const subNodeIds = getNodeIdsFromSubgraph(item.children);
216
226
  for (const subNodeId of subNodeIds) ensureNode(subNodeId, parentId, nd);
217
- if (subNodeIds.length > 0) endpointGroups.push(subNodeIds);
227
+ if (subNodeIds.length > 0) endpointGroups.push(subNodeIds.map((id) => ({ id })));
218
228
  }
219
229
  for (let i = 0; i < endpointGroups.length - 1; i++) {
220
230
  const left = endpointGroups[i];
221
231
  const right = endpointGroups[i + 1];
222
- for (const sourceId of left) for (const targetId of right) {
232
+ for (const source of left) for (const target of right) {
223
233
  const edge = {
224
234
  type: "edge",
225
235
  id: `e${edgeIdx++}`,
226
- sourceId,
227
- targetId,
236
+ sourceId: source.id,
237
+ targetId: target.id,
228
238
  label: mergedEdgeAttrs["label"] ?? "",
229
239
  data: void 0,
240
+ ...source.port && { sourcePort: source.port },
241
+ ...target.port && { targetPort: target.port },
230
242
  ...mergedEdgeAttrs["color"] && { color: mergedEdgeAttrs["color"] }
231
243
  };
232
244
  edges.push(edge);
@@ -23,13 +23,30 @@ function toGEXF(graph) {
23
23
  "@_id": "a_shape",
24
24
  "@_title": "shape",
25
25
  "@_type": "string"
26
+ },
27
+ {
28
+ "@_id": "a_ports",
29
+ "@_title": "ports",
30
+ "@_type": "string"
31
+ }
32
+ ];
33
+ const edgeAttrs = [
34
+ {
35
+ "@_id": "a_edgeData",
36
+ "@_title": "data",
37
+ "@_type": "string"
38
+ },
39
+ {
40
+ "@_id": "a_sourcePort",
41
+ "@_title": "sourcePort",
42
+ "@_type": "string"
43
+ },
44
+ {
45
+ "@_id": "a_targetPort",
46
+ "@_title": "targetPort",
47
+ "@_type": "string"
26
48
  }
27
49
  ];
28
- const edgeAttrs = [{
29
- "@_id": "a_edgeData",
30
- "@_title": "data",
31
- "@_type": "string"
32
- }];
33
50
  const nodes = graph.nodes.map((n) => {
34
51
  const attvalues = [];
35
52
  if (n.parentId) attvalues.push({
@@ -48,6 +65,10 @@ function toGEXF(graph) {
48
65
  "@_for": "a_shape",
49
66
  "@_value": n.shape
50
67
  });
68
+ if (n.ports !== void 0) attvalues.push({
69
+ "@_for": "a_ports",
70
+ "@_value": JSON.stringify(n.ports)
71
+ });
51
72
  const node = {
52
73
  "@_id": n.id,
53
74
  "@_label": n.label || n.id
@@ -80,6 +101,16 @@ function toGEXF(graph) {
80
101
  "@_for": "a_edgeData",
81
102
  "@_value": JSON.stringify(e.data)
82
103
  }] };
104
+ const edgeAttvalues = edge.attvalues?.attvalue ?? [];
105
+ if (e.sourcePort !== void 0) edgeAttvalues.push({
106
+ "@_for": "a_sourcePort",
107
+ "@_value": e.sourcePort
108
+ });
109
+ if (e.targetPort !== void 0) edgeAttvalues.push({
110
+ "@_for": "a_targetPort",
111
+ "@_value": e.targetPort
112
+ });
113
+ if (edgeAttvalues.length > 0) edge.attvalues = { attvalue: edgeAttvalues };
83
114
  if (e.color) {
84
115
  const hex$1 = e.color.replace("#", "");
85
116
  if (hex$1.length === 6) edge["viz:color"] = {
@@ -90,15 +121,6 @@ function toGEXF(graph) {
90
121
  }
91
122
  return edge;
92
123
  });
93
- const graphData = [];
94
- if (graph.initialNodeId) graphData.push({
95
- "@_for": "a_initialNodeId",
96
- "@_value": graph.initialNodeId
97
- });
98
- if (graph.data !== void 0) graphData.push({
99
- "@_for": "a_data",
100
- "@_value": JSON.stringify(graph.data)
101
- });
102
124
  const obj = {
103
125
  "?xml": {
104
126
  "@_version": "1.0",
@@ -111,6 +133,9 @@ function toGEXF(graph) {
111
133
  graph: {
112
134
  "@_defaultedgetype": graph.type === "directed" ? "directed" : "undirected",
113
135
  ...graph.id && { "@_id": graph.id },
136
+ ...graph.initialNodeId && { "@_initialNodeId": graph.initialNodeId },
137
+ ...graph.direction && { "@_direction": graph.direction },
138
+ ...graph.data !== void 0 && { "@_data": JSON.stringify(graph.data) },
114
139
  attributes: [{
115
140
  "@_class": "node",
116
141
  attribute: nodeAttrs
@@ -168,6 +193,7 @@ function fromGEXF(xml) {
168
193
  data: attvals["data"] !== void 0 ? tryParseJSON(attvals["data"]) : void 0
169
194
  };
170
195
  if (attvals["shape"]) node.shape = attvals["shape"];
196
+ if (attvals["ports"] !== void 0) node.ports = tryParseJSON(attvals["ports"]);
171
197
  const pos = n["viz:position"];
172
198
  if (pos) {
173
199
  node.x = Number(pos["@_x"] ?? 0);
@@ -199,7 +225,9 @@ function fromGEXF(xml) {
199
225
  sourceId: String(e["@_source"]),
200
226
  targetId: String(e["@_target"]),
201
227
  label: e["@_label"] ?? "",
202
- data: attvals["data"] !== void 0 ? tryParseJSON(attvals["data"]) : void 0
228
+ data: attvals["data"] !== void 0 ? tryParseJSON(attvals["data"]) : void 0,
229
+ ...attvals["sourcePort"] !== void 0 && { sourcePort: attvals["sourcePort"] },
230
+ ...attvals["targetPort"] !== void 0 && { targetPort: attvals["targetPort"] }
203
231
  };
204
232
  const color = e["viz:color"];
205
233
  if (color) {
@@ -213,10 +241,11 @@ function fromGEXF(xml) {
213
241
  return {
214
242
  id: String(graphEl["@_id"] ?? ""),
215
243
  type: graphType,
216
- initialNodeId: null,
244
+ initialNodeId: graphEl["@_initialNodeId"] ?? null,
217
245
  nodes,
218
246
  edges,
219
- data: void 0
247
+ data: graphEl["@_data"] !== void 0 ? tryParseJSON(String(graphEl["@_data"])) : void 0,
248
+ ...graphEl["@_direction"] && { direction: graphEl["@_direction"] }
220
249
  };
221
250
  }
222
251
  function asArray(val) {
@@ -40,6 +40,7 @@ function toGML(graph) {
40
40
  if (node.label) lines.push(`${indent} label ${gmlString(node.label)}`);
41
41
  if (node.initialNodeId) lines.push(`${indent} initialNodeId ${gmlString(node.initialNodeId)}`);
42
42
  if (node.data !== void 0) lines.push(`${indent} data ${gmlString(JSON.stringify(node.data))}`);
43
+ if (node.ports !== void 0) lines.push(`${indent} ports ${gmlString(JSON.stringify(node.ports))}`);
43
44
  if (node.shape) lines.push(`${indent} shape ${gmlString(node.shape)}`);
44
45
  if (node.color) lines.push(`${indent} color ${gmlString(node.color)}`);
45
46
  if (node.x !== void 0 || node.y !== void 0 || node.width !== void 0 || node.height !== void 0) {
@@ -63,6 +64,8 @@ function toGML(graph) {
63
64
  lines.push(` target ${gmlString(edge.targetId)}`);
64
65
  if (edge.label) lines.push(` label ${gmlString(edge.label)}`);
65
66
  if (edge.data !== void 0) lines.push(` data ${gmlString(JSON.stringify(edge.data))}`);
67
+ if (edge.sourcePort !== void 0) lines.push(` sourcePort ${gmlString(edge.sourcePort)}`);
68
+ if (edge.targetPort !== void 0) lines.push(` targetPort ${gmlString(edge.targetPort)}`);
66
69
  if (edge.color) lines.push(` color ${gmlString(edge.color)}`);
67
70
  lines.push(" ]");
68
71
  }
@@ -112,6 +115,7 @@ function fromGML(gml) {
112
115
  initialNodeId: n["initialNodeId"] ?? null,
113
116
  label: n["label"] ?? "",
114
117
  data: n["data"] !== void 0 ? tryParseJSON(n["data"]) : void 0,
118
+ ...n["ports"] !== void 0 && { ports: tryParseJSON(n["ports"]) },
115
119
  ...n["shape"] && { shape: n["shape"] },
116
120
  ...n["color"] && { color: n["color"] },
117
121
  ...gfx?.x !== void 0 && { x: gfx.x },
@@ -131,6 +135,8 @@ function fromGML(gml) {
131
135
  targetId: String(e["target"] ?? ""),
132
136
  label: e["label"] ?? "",
133
137
  data: e["data"] !== void 0 ? tryParseJSON(e["data"]) : void 0,
138
+ ...e["sourcePort"] !== void 0 && { sourcePort: String(e["sourcePort"]) },
139
+ ...e["targetPort"] !== void 0 && { targetPort: String(e["targetPort"]) },
134
140
  ...e["color"] && { color: e["color"] }
135
141
  });
136
142
  return {
@@ -38,6 +38,7 @@ function toJGF(graph) {
38
38
  if (n.height !== void 0) meta.height = n.height;
39
39
  if (n.shape) meta.shape = n.shape;
40
40
  if (n.color) meta.color = n.color;
41
+ if (n.ports !== void 0) meta.ports = n.ports;
41
42
  return {
42
43
  id: n.id,
43
44
  ...n.label && { label: n.label },
@@ -48,6 +49,8 @@ function toJGF(graph) {
48
49
  const meta = {};
49
50
  if (e.data !== void 0) meta.data = e.data;
50
51
  if (e.color) meta.color = e.color;
52
+ if (e.sourcePort !== void 0) meta.sourcePort = e.sourcePort;
53
+ if (e.targetPort !== void 0) meta.targetPort = e.targetPort;
51
54
  return {
52
55
  id: e.id,
53
56
  source: e.sourceId,
@@ -98,7 +101,8 @@ function fromJGF(jgf) {
98
101
  ...n.metadata?.width !== void 0 && { width: n.metadata.width },
99
102
  ...n.metadata?.height !== void 0 && { height: n.metadata.height },
100
103
  ...n.metadata?.shape && { shape: n.metadata.shape },
101
- ...n.metadata?.color && { color: n.metadata.color }
104
+ ...n.metadata?.color && { color: n.metadata.color },
105
+ ...n.metadata?.ports !== void 0 && { ports: n.metadata.ports }
102
106
  })),
103
107
  edges: g.edges.map((e, i) => ({
104
108
  type: "edge",
@@ -107,7 +111,9 @@ function fromJGF(jgf) {
107
111
  targetId: e.target,
108
112
  label: e.label ?? "",
109
113
  data: e.metadata?.data,
110
- ...e.metadata?.color && { color: e.metadata.color }
114
+ ...e.metadata?.color && { color: e.metadata.color },
115
+ ...e.metadata?.sourcePort !== void 0 && { sourcePort: e.metadata.sourcePort },
116
+ ...e.metadata?.targetPort !== void 0 && { targetPort: e.metadata.targetPort }
111
117
  }))
112
118
  };
113
119
  }
@@ -1,3 +1,4 @@
1
+ import { g as GraphNode, p as GraphEdge, u as Graph, y as GraphPort } from "./types-CnZ01raw.mjs";
1
2
  import * as z from "zod";
2
3
 
3
4
  //#region src/schemas.d.ts
@@ -126,5 +127,18 @@ declare const GraphSchema: z.ZodObject<{
126
127
  }>>;
127
128
  style: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
128
129
  }, z.core.$strip>;
130
+ interface GraphValidationIssue {
131
+ code: string;
132
+ message: string;
133
+ path: Array<string | number>;
134
+ }
135
+ declare function isGraphPort(value: unknown): value is GraphPort;
136
+ declare function isGraphNode(value: unknown): value is GraphNode;
137
+ declare function isGraphEdge(value: unknown): value is GraphEdge;
138
+ declare function isGraph(value: unknown): value is Graph;
139
+ declare function getGraphPortIssues(value: unknown): GraphValidationIssue[];
140
+ declare function getGraphNodeIssues(value: unknown): GraphValidationIssue[];
141
+ declare function getGraphEdgeIssues(value: unknown): GraphValidationIssue[];
142
+ declare function getGraphIssues(value: unknown): GraphValidationIssue[];
129
143
  //#endregion
130
- export { EdgeSchema, GraphSchema, NodeSchema, PortSchema };
144
+ export { EdgeSchema, GraphSchema, GraphValidationIssue, NodeSchema, PortSchema, getGraphEdgeIssues, getGraphIssues, getGraphNodeIssues, getGraphPortIssues, isGraph, isGraphEdge, isGraphNode, isGraphPort };
package/dist/schemas.mjs CHANGED
@@ -66,6 +66,39 @@ const GraphSchema = z.object({
66
66
  ]).optional(),
67
67
  style: StyleSchema.optional()
68
68
  });
69
+ function getValidationIssues(schema, value) {
70
+ const result = schema.safeParse(value);
71
+ if (result.success) return [];
72
+ return result.error.issues.map((issue) => ({
73
+ code: issue.code,
74
+ message: issue.message,
75
+ path: issue.path.map((segment) => typeof segment === "symbol" ? String(segment) : segment)
76
+ }));
77
+ }
78
+ function isGraphPort(value) {
79
+ return PortSchema.safeParse(value).success;
80
+ }
81
+ function isGraphNode(value) {
82
+ return NodeSchema.safeParse(value).success;
83
+ }
84
+ function isGraphEdge(value) {
85
+ return EdgeSchema.safeParse(value).success;
86
+ }
87
+ function isGraph(value) {
88
+ return GraphSchema.safeParse(value).success;
89
+ }
90
+ function getGraphPortIssues(value) {
91
+ return getValidationIssues(PortSchema, value);
92
+ }
93
+ function getGraphNodeIssues(value) {
94
+ return getValidationIssues(NodeSchema, value);
95
+ }
96
+ function getGraphEdgeIssues(value) {
97
+ return getValidationIssues(EdgeSchema, value);
98
+ }
99
+ function getGraphIssues(value) {
100
+ return getValidationIssues(GraphSchema, value);
101
+ }
69
102
 
70
103
  //#endregion
71
- export { EdgeSchema, GraphSchema, NodeSchema, PortSchema };
104
+ export { EdgeSchema, GraphSchema, NodeSchema, PortSchema, getGraphEdgeIssues, getGraphIssues, getGraphNodeIssues, getGraphPortIssues, isGraph, isGraphEdge, isGraphNode, isGraphPort };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@statelyai/graph",
3
3
  "type": "module",
4
- "version": "0.11.1",
4
+ "version": "0.12.0",
5
5
  "description": "A TypeScript-first graph library with plain JSON-serializable objects",
6
6
  "author": "David Khourshid <david@stately.ai>",
7
7
  "license": "MIT",