@dxos/app-graph 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6

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.
Files changed (45) hide show
  1. package/dist/lib/browser/chunk-AKBGYELG.mjs +1603 -0
  2. package/dist/lib/browser/chunk-AKBGYELG.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +17 -1276
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +39 -0
  7. package/dist/lib/browser/testing/index.mjs.map +7 -0
  8. package/dist/lib/node-esm/chunk-HR5S4XYH.mjs +1604 -0
  9. package/dist/lib/node-esm/chunk-HR5S4XYH.mjs.map +7 -0
  10. package/dist/lib/node-esm/index.mjs +17 -1276
  11. package/dist/lib/node-esm/index.mjs.map +4 -4
  12. package/dist/lib/node-esm/meta.json +1 -1
  13. package/dist/lib/node-esm/testing/index.mjs +40 -0
  14. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  15. package/dist/types/src/graph-builder.d.ts +11 -7
  16. package/dist/types/src/graph-builder.d.ts.map +1 -1
  17. package/dist/types/src/graph.d.ts +13 -17
  18. package/dist/types/src/graph.d.ts.map +1 -1
  19. package/dist/types/src/index.d.ts +1 -0
  20. package/dist/types/src/index.d.ts.map +1 -1
  21. package/dist/types/src/node-matcher.d.ts +43 -17
  22. package/dist/types/src/node-matcher.d.ts.map +1 -1
  23. package/dist/types/src/node.d.ts +21 -5
  24. package/dist/types/src/node.d.ts.map +1 -1
  25. package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
  26. package/dist/types/src/testing/index.d.ts +2 -0
  27. package/dist/types/src/testing/index.d.ts.map +1 -0
  28. package/dist/types/src/testing/setup-graph-builder.d.ts +31 -0
  29. package/dist/types/src/testing/setup-graph-builder.d.ts.map +1 -0
  30. package/dist/types/src/util.d.ts +39 -0
  31. package/dist/types/src/util.d.ts.map +1 -0
  32. package/dist/types/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +36 -26
  34. package/src/graph-builder.test.ts +569 -102
  35. package/src/graph-builder.ts +202 -74
  36. package/src/graph.test.ts +187 -52
  37. package/src/graph.ts +174 -98
  38. package/src/index.ts +1 -0
  39. package/src/node-matcher.ts +58 -28
  40. package/src/node.ts +46 -5
  41. package/src/stories/EchoGraph.stories.tsx +90 -61
  42. package/src/stories/Tree.tsx +1 -1
  43. package/src/testing/index.ts +5 -0
  44. package/src/testing/setup-graph-builder.ts +41 -0
  45. package/src/util.ts +95 -0
package/src/graph.test.ts CHANGED
@@ -12,7 +12,11 @@ import * as Node from './node';
12
12
 
13
13
  const exampleId = (id: number) => `dx:test:${id}`;
14
14
  const EXAMPLE_ID = exampleId(1);
15
- const EXAMPLE_TYPE = 'dxos.org/type/example';
15
+ const EXAMPLE_TYPE = 'org.dxos.type.example';
16
+ const CHILD_RELATION_KEY = Graph.relationKey('child');
17
+ const CHILD_INBOUND_RELATION_KEY = Graph.relationKey(Node.childRelation('inbound'));
18
+ const ACTIONS_RELATION_KEY = Graph.relationKey('action');
19
+ const ACTIONS_INBOUND_RELATION_KEY = Graph.relationKey(Node.actionRelation('inbound'));
16
20
 
17
21
  describe('Graph', () => {
18
22
  test('getGraph', () => {
@@ -100,6 +104,39 @@ describe('Graph', () => {
100
104
  }
101
105
  });
102
106
 
107
+ test('remove node with edges=true removes default inbound counterparts', () => {
108
+ const registry = Registry.make();
109
+ const graph = Graph.make({ registry });
110
+ Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
111
+ Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE });
112
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
113
+
114
+ Graph.removeNode(graph, exampleId(2), true);
115
+
116
+ const sourceEdges = registry.get(graph.edges(exampleId(1)));
117
+ expect(sourceEdges[CHILD_RELATION_KEY]).toEqual([]);
118
+ expect(sourceEdges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
119
+ });
120
+
121
+ test('remove node with edges=true removes typed inbound and outbound counterparts', () => {
122
+ const registry = Registry.make();
123
+ const graph = Graph.make({ registry });
124
+ Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
125
+ Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE });
126
+ Graph.addNode(graph, { id: exampleId(3), type: EXAMPLE_TYPE });
127
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'action' });
128
+ Graph.addEdge(graph, { source: exampleId(2), target: exampleId(3), relation: 'action' });
129
+
130
+ Graph.removeNode(graph, exampleId(2), true);
131
+
132
+ const sourceEdges = registry.get(graph.edges(exampleId(1)));
133
+ const targetEdges = registry.get(graph.edges(exampleId(3)));
134
+ expect(sourceEdges[ACTIONS_RELATION_KEY]).toEqual([]);
135
+ expect(sourceEdges[ACTIONS_INBOUND_RELATION_KEY]).toBeUndefined();
136
+ expect(targetEdges[ACTIONS_RELATION_KEY]).toBeUndefined();
137
+ expect(targetEdges[ACTIONS_INBOUND_RELATION_KEY]).toEqual([]);
138
+ });
139
+
103
140
  test('remove node curried', () => {
104
141
  const registry = Registry.make();
105
142
  const graph = Graph.make({ registry });
@@ -130,20 +167,29 @@ describe('Graph', () => {
130
167
  test('add edge', () => {
131
168
  const registry = Registry.make();
132
169
  const graph = Graph.make({ registry });
133
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2) });
134
- const edges = registry.get(graph.edges(exampleId(1)));
135
- expect(edges.inbound).toEqual([]);
136
- expect(edges.outbound).toEqual([exampleId(2)]);
170
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
171
+
172
+ {
173
+ const edges = registry.get(graph.edges(exampleId(1)));
174
+ expect(edges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
175
+ expect(edges[CHILD_RELATION_KEY]).toEqual([exampleId(2)]);
176
+ }
177
+
178
+ {
179
+ const edges = registry.get(graph.edges(exampleId(2)));
180
+ expect(edges[CHILD_INBOUND_RELATION_KEY]).toEqual([exampleId(1)]);
181
+ expect(edges[CHILD_RELATION_KEY]).toBeUndefined();
182
+ }
137
183
  });
138
184
 
139
185
  test('add edges is idempotent', () => {
140
186
  const registry = Registry.make();
141
187
  const graph = Graph.make({ registry });
142
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2) });
143
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2) });
188
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
189
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
144
190
  const edges = registry.get(graph.edges(exampleId(1)));
145
- expect(edges.inbound).toEqual([]);
146
- expect(edges.outbound).toEqual([exampleId(2)]);
191
+ expect(edges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
192
+ expect(edges[CHILD_RELATION_KEY]).toEqual([exampleId(2)]);
147
193
  });
148
194
 
149
195
  test('sort edges', () => {
@@ -151,17 +197,17 @@ describe('Graph', () => {
151
197
  const graph = Graph.make({ registry });
152
198
 
153
199
  {
154
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2) });
155
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(3) });
156
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(4) });
200
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
201
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(3), relation: 'child' });
202
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(4), relation: 'child' });
157
203
  const edges = registry.get(graph.edges(exampleId(1)));
158
- expect(edges.outbound).toEqual([exampleId(2), exampleId(3), exampleId(4)]);
204
+ expect(edges[CHILD_RELATION_KEY]).toEqual([exampleId(2), exampleId(3), exampleId(4)]);
159
205
  }
160
206
 
161
207
  {
162
- Graph.sortEdges(graph, exampleId(1), 'outbound', [exampleId(3), exampleId(2)]);
208
+ Graph.sortEdges(graph, exampleId(1), 'child', [exampleId(3), exampleId(2)]);
163
209
  const edges = registry.get(graph.edges(exampleId(1)));
164
- expect(edges.outbound).toEqual([exampleId(3), exampleId(2), exampleId(4)]);
210
+ expect(edges[CHILD_RELATION_KEY]).toEqual([exampleId(3), exampleId(2), exampleId(4)]);
165
211
  }
166
212
  });
167
213
 
@@ -170,17 +216,17 @@ describe('Graph', () => {
170
216
  const graph = Graph.make({ registry });
171
217
 
172
218
  {
173
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2) });
219
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
174
220
  const edges = registry.get(graph.edges(exampleId(1)));
175
- expect(edges.inbound).toEqual([]);
176
- expect(edges.outbound).toEqual([exampleId(2)]);
221
+ expect(edges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
222
+ expect(edges[CHILD_RELATION_KEY]).toEqual([exampleId(2)]);
177
223
  }
178
224
 
179
225
  {
180
- Graph.removeEdge(graph, { source: exampleId(1), target: exampleId(2) });
226
+ Graph.removeEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
181
227
  const edges = registry.get(graph.edges(exampleId(1)));
182
- expect(edges.inbound).toEqual([]);
183
- expect(edges.outbound).toEqual([]);
228
+ expect(edges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
229
+ expect(edges[CHILD_RELATION_KEY]).toEqual([]);
184
230
  }
185
231
  });
186
232
 
@@ -189,12 +235,42 @@ describe('Graph', () => {
189
235
  const graph = Graph.make({ registry });
190
236
  Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
191
237
  Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE });
192
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2) });
193
- const result = graph.pipe(Graph.removeEdge({ source: exampleId(1), target: exampleId(2) }));
238
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
239
+ const result = graph.pipe(Graph.removeEdge({ source: exampleId(1), target: exampleId(2), relation: 'child' }));
194
240
  expect(result).toEqual(graph);
195
241
  const edges = registry.get(graph.edges(exampleId(1)));
196
- expect(edges.inbound).toEqual([]);
197
- expect(edges.outbound).toEqual([]);
242
+ expect(edges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
243
+ expect(edges[CHILD_RELATION_KEY]).toEqual([]);
244
+ });
245
+
246
+ test('add edge with custom relation creates typed inbound inverse', () => {
247
+ const registry = Registry.make();
248
+ const graph = Graph.make({ registry });
249
+ Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
250
+ Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE });
251
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'action' });
252
+ const sourceEdges = registry.get(graph.edges(exampleId(1)));
253
+ expect(sourceEdges[ACTIONS_RELATION_KEY]).toEqual([exampleId(2)]);
254
+ expect(sourceEdges[CHILD_RELATION_KEY]).toBeUndefined();
255
+ const targetEdges = registry.get(graph.edges(exampleId(2)));
256
+ expect(targetEdges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
257
+ expect(targetEdges[ACTIONS_INBOUND_RELATION_KEY]).toEqual([exampleId(1)]);
258
+ const reverseConnections = registry.get(graph.connections(exampleId(2), Node.actionRelation('inbound')));
259
+ expect(reverseConnections.map(({ id }) => id)).toEqual([exampleId(1)]);
260
+ });
261
+
262
+ test('remove edge with custom relation removes typed inbound inverse', () => {
263
+ const registry = Registry.make();
264
+ const graph = Graph.make({ registry });
265
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'action' });
266
+ Graph.removeEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'action' });
267
+ const sourceEdges = registry.get(graph.edges(exampleId(1)));
268
+ expect(sourceEdges[ACTIONS_RELATION_KEY]).toEqual([]);
269
+ const targetEdges = registry.get(graph.edges(exampleId(2)));
270
+ expect(targetEdges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
271
+ expect(targetEdges[CHILD_RELATION_KEY]).toBeUndefined();
272
+ expect(targetEdges[ACTIONS_RELATION_KEY]).toBeUndefined();
273
+ expect(targetEdges[ACTIONS_INBOUND_RELATION_KEY]).toEqual([]);
198
274
  });
199
275
 
200
276
  test('get connections', () => {
@@ -202,8 +278,8 @@ describe('Graph', () => {
202
278
  const graph = Graph.make({ registry });
203
279
  Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
204
280
  Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE });
205
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2) });
206
- const nodes = registry.get(graph.connections(exampleId(1)));
281
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
282
+ const nodes = registry.get(graph.connections(exampleId(1), 'child'));
207
283
  expect(nodes).has.length(1);
208
284
  expect(nodes[0].id).toEqual(exampleId(2));
209
285
  });
@@ -228,8 +304,8 @@ describe('Graph', () => {
228
304
  test('connections updates', () => {
229
305
  const registry = Registry.make();
230
306
  const graph = Graph.make({ registry });
231
- assert.strictEqual(graph.connections(exampleId(1)), graph.connections(exampleId(1)));
232
- const childrenKey = graph.connections(exampleId(1));
307
+ assert.strictEqual(graph.connections(exampleId(1), 'child'), graph.connections(exampleId(1), 'child'));
308
+ const childrenKey = graph.connections(exampleId(1), 'child');
233
309
 
234
310
  let count = 0;
235
311
  const cancel = registry.subscribe(childrenKey, (_) => {
@@ -240,7 +316,7 @@ describe('Graph', () => {
240
316
  graph.pipe(
241
317
  Graph.addNode({ id: exampleId(1), type: EXAMPLE_TYPE }),
242
318
  Graph.addNode({ id: exampleId(2), type: EXAMPLE_TYPE }),
243
- Graph.addEdge({ source: exampleId(1), target: exampleId(2) }),
319
+ Graph.addEdge({ source: exampleId(1), target: exampleId(2), relation: 'child' }),
244
320
  );
245
321
 
246
322
  expect(count).toEqual(0);
@@ -262,12 +338,12 @@ describe('Graph', () => {
262
338
  expect(count).toEqual(2);
263
339
 
264
340
  // Connecting a node fires an update.
265
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(3) });
341
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(3), relation: 'child' });
266
342
  expect(count).toEqual(3);
267
343
 
268
344
  // Adding an edge connected to nothing fires an update.
269
345
  // TODO(wittjosiah): Is there a way to avoid this?
270
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(4) });
346
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(4), relation: 'child' });
271
347
  expect(count).toEqual(4);
272
348
 
273
349
  // Adding a node to an existing edge fires an update.
@@ -277,7 +353,7 @@ describe('Graph', () => {
277
353
  // Batching the edge and node updates fires a single update.
278
354
  Atom.batch(() => {
279
355
  graph.pipe(
280
- Graph.addEdge({ source: exampleId(1), target: exampleId(6) }),
356
+ Graph.addEdge({ source: exampleId(1), target: exampleId(6), relation: 'child' }),
281
357
  Graph.addNode({ id: exampleId(6), type: EXAMPLE_TYPE }),
282
358
  );
283
359
  });
@@ -295,7 +371,7 @@ describe('Graph', () => {
295
371
  { id: 'test2', type: 'test' },
296
372
  ],
297
373
  });
298
- Graph.addEdge(graph, { source: 'test1', target: 'test2' });
374
+ Graph.addEdge(graph, { source: 'test1', target: 'test2', relation: 'child' });
299
375
 
300
376
  const json = Graph.toJSON(graph);
301
377
  expect(json).to.deep.equal({
@@ -320,7 +396,7 @@ describe('Graph', () => {
320
396
  { id: 'test2', type: 'test' },
321
397
  ],
322
398
  });
323
- Graph.addEdge(graph, { source: 'test1', target: 'test2' });
399
+ Graph.addEdge(graph, { source: 'test1', target: 'test2', relation: 'child' });
324
400
 
325
401
  let json: any;
326
402
  const cancel = registry.subscribe(graph.json(), (_) => {
@@ -339,7 +415,7 @@ describe('Graph', () => {
339
415
  });
340
416
 
341
417
  Graph.addNode(graph, { id: 'test3', type: 'test' });
342
- Graph.addEdge(graph, { source: 'root', target: 'test3' });
418
+ Graph.addEdge(graph, { source: 'root', target: 'test3', relation: 'child' });
343
419
  expect(json).to.deep.equal({
344
420
  id: Node.RootId,
345
421
  type: Node.RootType,
@@ -361,7 +437,7 @@ describe('Graph', () => {
361
437
  { id: exampleId(2), type: EXAMPLE_TYPE },
362
438
  ],
363
439
  });
364
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2) });
440
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
365
441
 
366
442
  expect(Graph.getPath(graph, { target: exampleId(2) }).pipe(Option.getOrNull)).to.deep.equal([
367
443
  'root',
@@ -385,7 +461,7 @@ describe('Graph', () => {
385
461
  { id: exampleId(2), type: EXAMPLE_TYPE },
386
462
  ],
387
463
  });
388
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2) });
464
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
389
465
  const path = graph.pipe(Graph.getPath({ target: exampleId(2) }));
390
466
  expect(path.pipe(Option.getOrNull)).to.deep.equal(['root', exampleId(1), exampleId(2)]);
391
467
  });
@@ -404,6 +480,7 @@ describe('Graph', () => {
404
480
 
405
481
  const nodes: string[] = [];
406
482
  Graph.traverse(graph, {
483
+ relation: 'child',
407
484
  visitor: (node) => {
408
485
  nodes.push(node.id);
409
486
  },
@@ -421,10 +498,11 @@ describe('Graph', () => {
421
498
  { id: 'test2', type: 'test' },
422
499
  ],
423
500
  });
424
- Graph.addEdge(graph, { source: 'test1', target: 'root' });
501
+ Graph.addEdge(graph, { source: 'test1', target: 'root', relation: 'child' });
425
502
 
426
503
  const nodes: string[] = [];
427
504
  Graph.traverse(graph, {
505
+ relation: 'child',
428
506
  visitor: (node) => {
429
507
  nodes.push(node.id);
430
508
  },
@@ -449,6 +527,7 @@ describe('Graph', () => {
449
527
  const nodes: string[] = [];
450
528
  Graph.traverse(graph, {
451
529
  source: 'test2',
530
+ relation: 'child',
452
531
  visitor: (node) => {
453
532
  nodes.push(node.id);
454
533
  },
@@ -473,7 +552,7 @@ describe('Graph', () => {
473
552
  const nodes: string[] = [];
474
553
  Graph.traverse(graph, {
475
554
  source: 'test2',
476
- relation: 'inbound',
555
+ relation: Node.childRelation('inbound'),
477
556
  visitor: (node) => {
478
557
  nodes.push(node.id);
479
558
  },
@@ -481,6 +560,23 @@ describe('Graph', () => {
481
560
  expect(nodes).to.deep.equal(['test2', 'test1', 'root']);
482
561
  });
483
562
 
563
+ test('traversal can follow typed inbound edges', () => {
564
+ const graph = Graph.make();
565
+ Graph.addNode(graph, { id: 'host', type: 'test' });
566
+ Graph.addNode(graph, { id: 'action', type: 'test' });
567
+ Graph.addEdge(graph, { source: 'host', target: 'action', relation: 'action' });
568
+
569
+ const nodes: string[] = [];
570
+ Graph.traverse(graph, {
571
+ source: 'action',
572
+ relation: Node.actionRelation('inbound'),
573
+ visitor: (node) => {
574
+ nodes.push(node.id);
575
+ },
576
+ });
577
+ expect(nodes).to.deep.equal(['action', 'host']);
578
+ });
579
+
484
580
  test('traversal can be terminated early', () => {
485
581
  const graph = Graph.make();
486
582
  Graph.addNode(graph, {
@@ -494,6 +590,7 @@ describe('Graph', () => {
494
590
 
495
591
  const nodes: string[] = [];
496
592
  Graph.traverse(graph, {
593
+ relation: 'child',
497
594
  visitor: (node) => {
498
595
  if (nodes.length === 2) {
499
596
  return false;
@@ -505,6 +602,26 @@ describe('Graph', () => {
505
602
  expect(nodes).to.deep.equal(['root', 'test1']);
506
603
  });
507
604
 
605
+ test('traversal with multiple relations follows all edge types', () => {
606
+ const graph = Graph.make();
607
+ Graph.addNode(graph, {
608
+ id: Node.RootId,
609
+ type: Node.RootType,
610
+ nodes: [{ id: 'child1', type: 'test' }],
611
+ });
612
+ Graph.addNode(graph, { id: 'action1', type: Node.ActionType });
613
+ Graph.addEdge(graph, { source: Node.RootId, target: 'action1', relation: 'action' });
614
+
615
+ const nodes: string[] = [];
616
+ Graph.traverse(graph, {
617
+ relation: ['child', 'action'],
618
+ visitor: (node) => {
619
+ nodes.push(node.id);
620
+ },
621
+ });
622
+ expect(nodes).to.deep.equal(['root', 'child1', 'action1']);
623
+ });
624
+
508
625
  test('traverse curried', () => {
509
626
  const graph = Graph.make();
510
627
  Graph.addNode(graph, {
@@ -513,11 +630,12 @@ describe('Graph', () => {
513
630
  nodes: [{ id: 'test1', type: 'test' }],
514
631
  });
515
632
  Graph.addNode(graph, { id: 'test2', type: 'test' });
516
- Graph.addEdge(graph, { source: 'test1', target: 'test2' });
633
+ Graph.addEdge(graph, { source: 'test1', target: 'test2', relation: 'child' });
517
634
  const nodes: string[] = [];
518
635
  graph.pipe(
519
636
  Graph.traverse({
520
637
  source: Node.RootId,
638
+ relation: 'child',
521
639
  visitor: (node, _path) => {
522
640
  nodes.push(node.id);
523
641
  },
@@ -553,15 +671,15 @@ describe('Graph', () => {
553
671
  Graph.addNode(graph, { id: exampleId(3), type: EXAMPLE_TYPE });
554
672
  const result = graph.pipe(
555
673
  Graph.addEdges([
556
- { source: exampleId(1), target: exampleId(2) },
557
- { source: exampleId(1), target: exampleId(3) },
674
+ { source: exampleId(1), target: exampleId(2), relation: 'child' },
675
+ { source: exampleId(1), target: exampleId(3), relation: 'child' },
558
676
  ]),
559
677
  );
560
678
  expect(result).toEqual(graph);
561
679
  const edges = registry.get(graph.edges(exampleId(1)));
562
- expect(edges.outbound).to.have.length(2);
563
- expect(edges.outbound).to.include(exampleId(2));
564
- expect(edges.outbound).to.include(exampleId(3));
680
+ expect(edges[CHILD_RELATION_KEY]).to.have.length(2);
681
+ expect(edges[CHILD_RELATION_KEY]).to.include(exampleId(2));
682
+ expect(edges[CHILD_RELATION_KEY]).to.include(exampleId(3));
565
683
  });
566
684
 
567
685
  test('remove nodes curried', () => {
@@ -583,17 +701,17 @@ describe('Graph', () => {
583
701
  Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
584
702
  Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE });
585
703
  Graph.addNode(graph, { id: exampleId(3), type: EXAMPLE_TYPE });
586
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2) });
587
- Graph.addEdge(graph, { source: exampleId(1), target: exampleId(3) });
704
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
705
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(3), relation: 'child' });
588
706
  const result = graph.pipe(
589
707
  Graph.removeEdges([
590
- { source: exampleId(1), target: exampleId(2) },
591
- { source: exampleId(1), target: exampleId(3) },
708
+ { source: exampleId(1), target: exampleId(2), relation: 'child' },
709
+ { source: exampleId(1), target: exampleId(3), relation: 'child' },
592
710
  ]),
593
711
  );
594
712
  expect(result).toEqual(graph);
595
713
  const edges = registry.get(graph.edges(exampleId(1)));
596
- expect(edges.outbound).to.have.length(0);
714
+ expect(edges[CHILD_RELATION_KEY]).to.have.length(0);
597
715
  });
598
716
 
599
717
  test('expand curried', async () => {
@@ -611,10 +729,27 @@ describe('Graph', () => {
611
729
  },
612
730
  }),
613
731
  );
614
- await graph.pipe(Graph.expand(Node.RootId));
732
+ await graph.pipe(Graph.expand(Node.RootId, 'child'));
615
733
  expect(expandCalled).to.be.true;
616
734
  });
617
735
 
736
+ test('expand defers for non-existent node and applies when node is added', () => {
737
+ const registry = Registry.make();
738
+ const expandCalls: [string, Node.Relation][] = [];
739
+ const graph = Graph.make({
740
+ registry,
741
+ onExpand: (id, relation) => expandCalls.push([id, relation]),
742
+ });
743
+ const childId = 'child';
744
+ expect(Option.isNone(registry.get(graph.node(childId)))).to.be.true;
745
+
746
+ Graph.expand(graph, childId, 'child');
747
+ expect(expandCalls).to.deep.equal([]);
748
+
749
+ Graph.addNode(graph, { id: childId, type: EXAMPLE_TYPE });
750
+ expect(expandCalls).to.deep.equal([[childId, Node.childRelation()]]);
751
+ });
752
+
618
753
  test('initialize curried', async () => {
619
754
  const registry = Registry.make();
620
755
  const builder = GraphBuilder.make({ registry });