@eventcatalog/core 5.7.4 → 5.7.5

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.
@@ -85,13 +85,13 @@ export const getNodesAndEdgesForChannelChain = ({
85
85
  }
86
86
  }
87
87
 
88
- // Connect the last channel to the target
88
+ // Connect the last channel to the target, which accepts a command or query (it handles them) and consumes an event
89
89
  edges.push(
90
90
  createEdge({
91
91
  id: generatedIdForEdge(channelChain[channelChain.length - 1], target),
92
92
  source: generateIdForNode(channelChain[channelChain.length - 1]),
93
93
  target: generateIdForNode(target),
94
- label: 'consumes',
94
+ label: ['commands', 'queries'].includes(source.collection) ? 'accepts' : 'consumes',
95
95
  data: { customColor: getColorFromString(source.data.id) },
96
96
  })
97
97
  );
@@ -29,6 +29,8 @@ type Tree = { domain: any; groupId: string; parentId?: string }[];
29
29
  type Owners = Map<string, { domain: any; system?: any }>;
30
30
 
31
31
  const MESSAGE_TYPES = new Set(['events', 'commands', 'queries']);
32
+ // Commands and queries belong to what receives them (it handles them), as events do to what sends them
33
+ const RECEIVER_OWNED_TYPES = new Set(['commands', 'queries']);
32
34
  const SERVICE_TYPES = new Set(['services', 'agents']);
33
35
 
34
36
  const systemGroupId = (system: any) => `system-group-${system.data.id}-${system.data.version}`;
@@ -44,6 +46,45 @@ const isInside = (nodes: Map<string, any>, nodeId: string, groupId: string) => {
44
46
  return false;
45
47
  };
46
48
 
49
+ /**
50
+ * Whether a node (or the services another domain stands for, `receivedBy`) says
51
+ * it receives a message: its id, and its version (or range, or latest). Undefined
52
+ * when it doesn't say what it receives
53
+ */
54
+ const saysItReceives = (node: any, message: any, receivedBy?: any[]): boolean | undefined => {
55
+ const receives = (node?.data?.service ?? node?.data?.agent)?.receives ?? receivedBy;
56
+ if (!receives) return undefined;
57
+ const { id, version } = message?.data?.message ?? {};
58
+ return receives.some((pointer: any) => {
59
+ const received = pointer.data ?? pointer;
60
+ return received.id === id && versionMatches(version ?? '', received.version ?? 'latest');
61
+ });
62
+ };
63
+
64
+ /**
65
+ * What a message reaches, through any channels (not the channels themselves).
66
+ * Past a channel, only what doesn't say it receives something else: a channel
67
+ * can carry other messages, to consumers of their own
68
+ */
69
+ const receiversThroughChannels = (messageId: string, nodes: Map<string, any>, receiversOf: Map<string, string[]>) => {
70
+ const message = nodes.get(messageId);
71
+ const receivers: string[] = [];
72
+ const seen = new Set([messageId]);
73
+ for (let current = [messageId]; current.length > 0; ) {
74
+ const throughChannel = current[0] !== messageId;
75
+ const next = current.flatMap((nodeId) => receiversOf.get(nodeId) ?? []).filter((nodeId) => !seen.has(nodeId));
76
+ next.forEach((nodeId) => seen.add(nodeId));
77
+ receivers.push(
78
+ ...next.filter(
79
+ (nodeId) =>
80
+ nodes.get(nodeId)?.type !== 'channels' && (!throughChannel || saysItReceives(nodes.get(nodeId), message) !== false)
81
+ )
82
+ );
83
+ current = next.filter((nodeId) => nodes.get(nodeId)?.type === 'channels');
84
+ }
85
+ return receivers;
86
+ };
87
+
47
88
  /** A system shown as a single node (not expanded into what's inside it) */
48
89
  const createSystemNode = (system: any, parentId?: string) => ({
49
90
  id: generateIdForNode(system),
@@ -181,16 +222,8 @@ export const collapseSystems = (
181
222
  isMessage(sender) ? [sender] : isChannel(sender) ? messagesThrough(sender, seen) : []
182
223
  );
183
224
  };
184
- // Whether a node (or the services another domain stands for) says it receives a
185
- // message: its id, and its version (or range, or latest)
186
- const receives = (nodeId: string, messageId: string) => {
187
- const data = nodesById.get(nodeId)?.data;
188
- const message = nodesById.get(messageId)?.data?.message;
189
- return ((data?.service ?? data?.agent)?.receives ?? receivedBy.get(nodeId) ?? []).some((pointer: any) => {
190
- const { id, version } = pointer.data ?? pointer;
191
- return id === message?.id && versionMatches(message?.version ?? '', version ?? 'latest');
192
- });
193
- };
225
+ const receives = (nodeId: string, messageId: string) =>
226
+ saysItReceives(nodesById.get(nodeId), nodesById.get(messageId), receivedBy.get(nodeId)) ?? false;
194
227
 
195
228
  // Where an edge is from and the messages it carries. A channel is one node for
196
229
  // all its messages, so an edge out of one is from each message sent through it
@@ -279,7 +312,7 @@ export const buildDomainTree = (domain: any, findDomain: (id: string, version?:
279
312
  return tree;
280
313
  };
281
314
 
282
- /** Which domain (and system) each service belongs to, and the domain each subdomain is in */
315
+ /** Which domain (and system) each service or agent belongs to, and the domain each subdomain is in */
283
316
  export const collectOwners = (domains: any[]) => {
284
317
  const owners: Owners = new Map();
285
318
  const parentDomainOf = new Map<string, any>();
@@ -291,9 +324,9 @@ export const collectOwners = (domains: any[]) => {
291
324
  if (!parentDomainOf.has(subdomain.data.id)) parentDomainOf.set(subdomain.data.id, d);
292
325
  });
293
326
  });
294
- // Services outside systems, unless a system already claims them
327
+ // Services outside systems, unless a system already claims them, and agents (systems don't have them)
295
328
  domains.forEach((d: any) =>
296
- (d.data.services || []).forEach((service: any) => {
329
+ [...(d.data.services || []), ...(d.data.agents || [])].forEach((service: any) => {
297
330
  const serviceId = generateIdForNode(service);
298
331
  if (!owners.has(serviceId)) owners.set(serviceId, { domain: d });
299
332
  })
@@ -444,7 +477,12 @@ export const buildDomainGraph = ({
444
477
  }
445
478
  // Messages, channels, data stores: kept in the first graph they appear in
446
479
  if (nodes.has(node.id)) return;
447
- nodes.set(node.id, node.parentId || !inside || SERVICE_TYPES.has(node.type) ? node : { ...node, parentId: inside });
480
+ // A service or agent in no domain isn't in the system whose graph it came from
481
+ if (SERVICE_TYPES.has(node.type)) {
482
+ nodes.set(node.id, { ...node, parentId: undefined, extent: undefined });
483
+ return;
484
+ }
485
+ nodes.set(node.id, node.parentId || !inside ? node : { ...node, parentId: inside });
448
486
  });
449
487
 
450
488
  systems.forEach(({ system, groupId: domainGroup }, index) => {
@@ -498,8 +536,8 @@ export const buildDomainGraph = ({
498
536
  if (boxId) replacedBy.set(replaced, boxId);
499
537
  });
500
538
 
501
- const graphEdges = connectEdges(edges, new Map([...cardBoxes, ...replacedBy]), nodes);
502
- placeMessagesWithPublishers(nodes, graphEdges, new Set(domainMessages.keys()));
539
+ const graphEdges = dropReceivesThroughChannels(connectEdges(edges, new Map([...cardBoxes, ...replacedBy]), nodes), nodes);
540
+ placeMessagesWithOwners(nodes, graphEdges, new Set(domainMessages.keys()));
503
541
  // What the services each other domain stands for receive, by the node shown for the domain
504
542
  const receivedBy = new Map<string, any[]>();
505
543
  replacedBy.forEach((nodeId, replaced) =>
@@ -546,14 +584,6 @@ const addDomainMessages = ({
546
584
  const treeDomainIds = new Set(tree.map((entry) => entry.domain.data.id));
547
585
  const otherDomainIds = new Set<string>();
548
586
  const domainMessages = new Map<string, Set<string>>();
549
- // The domain each agent is in (owners only has services): the tree's versions first
550
- const agentOwners = new Map<string, any>();
551
- [...tree.map((entry) => entry.domain), ...domains].forEach((domain) =>
552
- (domain.data.agents || []).forEach((agent: any) => {
553
- const agentId = generateIdForNode(agent);
554
- if (!agentOwners.has(agentId)) agentOwners.set(agentId, domain);
555
- })
556
- );
557
587
  // Whether a domain or service sends (or receives) the message (their messages are already looked up)
558
588
  const lists = (resource: any, key: 'sends' | 'receives', messageId: string) =>
559
589
  (resource.data[key] || []).some((message: any) => message.data && generateIdForNode(message) === messageId);
@@ -579,19 +609,19 @@ const addDomainMessages = ({
579
609
  return false;
580
610
  };
581
611
  // Whether a service in a (sub)domain's box says it sends (or receives) the message: e.g. one it sends in a
582
- // message group, which its graph shows as the group rather than the message. Services only: agents in a
583
- // domain aren't drawn on its diagram
612
+ // message group, which its graph shows as the group rather than the message. Services only: a domain's
613
+ // agents are only drawn when another graph brings them in
584
614
  const groupOfTreeDomain = new Map(tree.map((entry) => [entry.domain.data.id, entry.groupId]));
585
615
  const declaredIn = (groupId: string, key: 'sends' | 'receives', messageId: string) =>
586
616
  catalogServices.some((service) => {
587
- if (!lists(service, key, messageId)) return false;
617
+ if (service.collection === 'agents' || !lists(service, key, messageId)) return false;
588
618
  const ownerGroup = groupOfTreeDomain.get(owners.get(generateIdForNode(service))?.domain.data.id);
589
619
  return !!ownerGroup && (ownerGroup === groupId || isInside(nodes, ownerGroup, groupId));
590
620
  });
591
621
  // The node standing for a service (or agent) on the other side, or none when it's in the domain
592
622
  const otherSide = (service: any) => {
593
623
  const serviceId = generateIdForNode(service);
594
- const ownerDomain = owners.get(serviceId)?.domain ?? agentOwners.get(serviceId);
624
+ const ownerDomain = owners.get(serviceId)?.domain;
595
625
  if (ownerDomain) return treeDomainIds.has(ownerDomain.data.id) ? undefined : addOther(ownerDomain);
596
626
  if (!nodes.has(serviceId)) {
597
627
  nodes.set(serviceId, {
@@ -675,25 +705,36 @@ const addDomainMessages = ({
675
705
  /**
676
706
  * Messages are placed in the first graph they appear in, which can be a
677
707
  * system consuming them. Moves each message into the system (or domain) of the
678
- * services publishing it, when they're all in the same one, and each channel
679
- * with the messages sent through it. Messages a domain
708
+ * services it belongs to, when they're all in the same one: an event's
709
+ * publishers, a command's or query's receivers (through any channels). Each
710
+ * channel goes with the messages sent through it. Messages a domain
680
711
  * sends or receives itself (`kept`) stay outside its box, where its edge to them
681
712
  * is drawn.
682
713
  */
683
- export const placeMessagesWithPublishers = (nodes: Map<string, any>, edges: any[], kept: Set<string> = new Set()) => {
684
- const publisherGroups = new Map<string, Set<string | undefined>>();
714
+ export const placeMessagesWithOwners = (nodes: Map<string, any>, edges: any[], kept: Set<string> = new Set()) => {
715
+ const ownerGroups = new Map<string, Set<string | undefined>>();
716
+ const addOwner = (messageId: string, owner: any) =>
717
+ ownerGroups.set(messageId, (ownerGroups.get(messageId) ?? new Set()).add(owner.parentId));
718
+ const receiversOf = new Map<string, string[]>();
719
+ edges.forEach((edge) => receiversOf.set(edge.source, [...(receiversOf.get(edge.source) ?? []), edge.target]));
685
720
  edges.forEach((edge) => {
686
721
  const publisher = nodes.get(edge.source);
687
- if (!MESSAGE_TYPES.has(nodes.get(edge.target)?.type) || !SERVICE_TYPES.has(publisher?.type)) return;
688
- const groups = publisherGroups.get(edge.target) || new Set();
689
- groups.add(publisher.parentId);
690
- publisherGroups.set(edge.target, groups);
722
+ const type = nodes.get(edge.target)?.type;
723
+ if (!MESSAGE_TYPES.has(type) || RECEIVER_OWNED_TYPES.has(type) || !SERVICE_TYPES.has(publisher?.type)) return;
724
+ addOwner(edge.target, publisher);
725
+ });
726
+ nodes.forEach((message, messageId) => {
727
+ if (!RECEIVER_OWNED_TYPES.has(message.type)) return;
728
+ receiversThroughChannels(messageId, nodes, receiversOf)
729
+ .filter((nodeId) => SERVICE_TYPES.has(nodes.get(nodeId)?.type))
730
+ .forEach((nodeId) => addOwner(messageId, nodes.get(nodeId)));
691
731
  });
692
- publisherGroups.forEach((groups, messageId) => {
732
+ ownerGroups.forEach((groups, messageId) => {
693
733
  if (kept.has(messageId)) return;
694
734
  const [parentId] = [...groups];
695
735
  const message = nodes.get(messageId);
696
- if (groups.size === 1 && parentId && message.parentId !== parentId) {
736
+ // Owners are drawn nodes, so no parent means outside every box (e.g. a service or agent in no domain)
737
+ if (groups.size === 1 && message.parentId !== parentId) {
697
738
  nodes.set(messageId, { ...message, parentId, extent: undefined });
698
739
  }
699
740
  });
@@ -705,9 +746,10 @@ export const placeMessagesWithPublishers = (nodes: Map<string, any>, edges: any[
705
746
  moved = false;
706
747
  nodes.forEach((channel, channelId) => {
707
748
  if (channel.type !== 'channels' || kept.has(channelId)) return;
708
- const groups = new Set((sendersOf.get(channelId) ?? []).map((sender) => nodes.get(sender)?.parentId));
749
+ const senders = (sendersOf.get(channelId) ?? []).filter((sender) => nodes.has(sender));
750
+ const groups = new Set(senders.map((sender) => nodes.get(sender).parentId));
709
751
  const [parentId] = [...groups];
710
- if (groups.size === 1 && parentId && channel.parentId !== parentId) {
752
+ if (groups.size === 1 && channel.parentId !== parentId) {
711
753
  nodes.set(channelId, { ...channel, parentId, extent: undefined });
712
754
  moved = true;
713
755
  }
@@ -715,6 +757,27 @@ export const placeMessagesWithPublishers = (nodes: Map<string, any>, edges: any[
715
757
  }
716
758
  };
717
759
 
760
+ /**
761
+ * Drops the edges drawn for domains receiving a message themselves (`domain-receives-`)
762
+ * when the message already reaches them through its channels, e.g. another
763
+ * domain listing a command in its `receives` whose service receives it from a
764
+ * queue: the domain is drawn receiving it from the queue, not from the command too.
765
+ */
766
+ const dropReceivesThroughChannels = (edges: any[], nodes: Map<string, any>) => {
767
+ const isChannel = (nodeId: string) => nodes.get(nodeId)?.type === 'channels';
768
+ const reachesThroughChannels = (messageId: string, targetId: string) => {
769
+ const seen = new Set<string>();
770
+ for (let current = [messageId]; current.length > 0; ) {
771
+ const next = edges.filter((edge) => current.includes(edge.source) && isChannel(edge.target) && !seen.has(edge.target));
772
+ if (next.some((edge) => edges.some((out) => out.source === edge.target && out.target === targetId))) return true;
773
+ next.forEach((edge) => seen.add(edge.target));
774
+ current = next.map((edge) => edge.target);
775
+ }
776
+ return false;
777
+ };
778
+ return edges.filter((edge) => !edge.id.startsWith('domain-receives-') || !reachesThroughChannels(edge.source, edge.target));
779
+ };
780
+
718
781
  /**
719
782
  * Edges pointed at the nodes replacing their ends, dropping duplicates, ones to
720
783
  * nowhere and ones between a domain and a subdomain in it.
@@ -766,15 +829,19 @@ export const addContext = (contexts: Graph[], systems: { system: any }[], nodes:
766
829
  };
767
830
 
768
831
  /**
769
- * Marks cross-domain communication. Messages and channels only other domains
770
- * send to (e.g. a message this domain consumes, and the topic it comes
832
+ * Marks cross-domain communication. Events and channels only other domains
833
+ * send to (e.g. an event this domain consumes, and the topic it comes
771
834
  * through) are another domain's: they sit outside this domain, between the
772
835
  * domain sending them and their consumers, rather than in the system that
773
- * consumes them. They, and this domain's messages and channels that reach
836
+ * consumes them. Commands and queries belong to what receives them, so those
837
+ * (and the channels only they're sent through) only other domains receive are
838
+ * another domain's, and those this domain receives are its own, whoever sends
839
+ * them. Messages and channels other domains send, and this domain's that reach
774
840
  * other domains, are on a cross-domain path (`data.crossDomain`), as are the
775
841
  * edges to and from them and other domains.
776
842
  */
777
843
  export const markCrossDomain = (graph: Graph, otherDomainIds: Set<string>): Graph => {
844
+ const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
778
845
  const sendersOf = new Map<string, string[]>();
779
846
  const receiversOf = new Map<string, string[]>();
780
847
  graph.edges.forEach((edge) => {
@@ -801,16 +868,45 @@ export const markCrossDomain = (graph: Graph, otherDomainIds: Set<string>): Grap
801
868
  return reached;
802
869
  };
803
870
  // Sent only by other domains (or their messages and channels)
804
- const external = spread(new Set(otherDomainIds), (nodeId, reached) => {
871
+ const sentByOthers = spread(new Set(otherDomainIds), (nodeId, reached) => {
805
872
  const senders = sendersOf.get(nodeId) || [];
806
873
  return senders.length > 0 && senders.every((sender) => reached.has(sender));
807
874
  });
875
+ // Received only by other domains (or their messages and channels)
876
+ const receivedByOthers = spread(new Set(otherDomainIds), (nodeId, reached) => {
877
+ const receivers = receiversOf.get(nodeId) || [];
878
+ return receivers.length > 0 && receivers.every((receiver) => reached.has(receiver));
879
+ });
808
880
  // Reaching other domains (directly, or through messages and channels that do)
809
- const outgoing = spread(new Set(external), (nodeId, reached) =>
881
+ const outgoing = spread(new Set(sentByOthers), (nodeId, reached) =>
810
882
  (receiversOf.get(nodeId) || []).some((receiver) => reached.has(receiver))
811
883
  );
812
884
 
813
- const isExternalCarrier = (node: any) => external.has(node.id) && !otherDomainIds.has(node.id);
885
+ // A command or query, or a channel only they're sent through (by channel, worked out once: routes can split and
886
+ // join again)
887
+ const receiverOwnedChannels = new Map<string, boolean>();
888
+ const isReceiverOwned = (nodeId: string): boolean => {
889
+ const node = nodesById.get(nodeId);
890
+ if (MESSAGE_TYPES.has(node?.type)) return RECEIVER_OWNED_TYPES.has(node.type);
891
+ if (node?.type !== 'channels') return false;
892
+ if (!receiverOwnedChannels.has(nodeId)) {
893
+ // Not the receiver's while it's being worked out, so a loop of channels ends
894
+ receiverOwnedChannels.set(nodeId, false);
895
+ const carried = (sendersOf.get(nodeId) || []).filter((sender) => nodesById.has(sender) && isCarrier(nodesById.get(sender)));
896
+ receiverOwnedChannels.set(nodeId, carried.length > 0 && carried.every(isReceiverOwned));
897
+ }
898
+ return receiverOwnedChannels.get(nodeId)!;
899
+ };
900
+ // A command or query only other domains receive (not counting consumers of other messages on its channels)
901
+ const receivedOnlyByOthers = (messageId: string) => {
902
+ const receivers = receiversThroughChannels(messageId, nodesById, receiversOf);
903
+ return receivers.length > 0 && receivers.every((receiver) => otherDomainIds.has(receiver));
904
+ };
905
+ const isExternalCarrier = (node: any) => {
906
+ if (!isCarrier(node) || otherDomainIds.has(node.id)) return false;
907
+ if (RECEIVER_OWNED_TYPES.has(node.type)) return receivedOnlyByOthers(node.id);
908
+ return (isReceiverOwned(node.id) ? receivedByOthers : sentByOthers).has(node.id);
909
+ };
814
910
  const onCrossDomainPath = (nodeId: string) => outgoing.has(nodeId);
815
911
  return {
816
912
  nodes: graph.nodes.map((node) => {
@@ -858,11 +954,8 @@ export const getNodesAndEdges = (props: NodesAndEdgesProps): Promise<DomainLevel
858
954
  * or its latest when no domain has it: who sends or receives a domain's own
859
955
  * messages is matched against these.
860
956
  */
861
- const inVersionsUsed = (resources: any[], domains: any[], owners: Owners) => {
862
- const used = new Set([
863
- ...owners.keys(),
864
- ...domains.flatMap((domain) => (domain.data.agents || []).map((agent: any) => generateIdForNode(agent))),
865
- ]);
957
+ const inVersionsUsed = (resources: any[], owners: Owners) => {
958
+ const used = new Set(owners.keys());
866
959
  const key = (resource: any) => `${resource.collection}:${resource.data.id}`;
867
960
  const usedKeys = new Set(resources.filter((resource) => used.has(generateIdForNode(resource))).map(key));
868
961
  return resources.filter((resource) =>
@@ -892,11 +985,11 @@ const buildLevels = async ({ id, version, mode = 'full', channelRenderMode }: No
892
985
  const tree = buildDomainTree(domain, findDomain);
893
986
  // The tree's domains last, so the versions shown own their services
894
987
  const { owners, parentDomainOf } = collectOwners([...domains, ...tree.map((entry) => entry.domain)]);
895
- // A service the domain (or a subdomain) lists itself is shown as its own,
896
- // even when a system in another domain has it too
988
+ // A service (or agent) the domain or a subdomain lists itself is shown as its
989
+ // own, even when a system in another domain has it too
897
990
  const treeDomainIds = new Set(tree.map((entry) => entry.domain.data.id));
898
991
  tree.forEach(({ domain: d }) =>
899
- (d.data.services || []).forEach((service: any) => {
992
+ [...(d.data.services || []), ...(d.data.agents || [])].forEach((service: any) => {
900
993
  const serviceId = generateIdForNode(service);
901
994
  if (!treeDomainIds.has(owners.get(serviceId)?.domain.data.id)) owners.set(serviceId, { domain: d });
902
995
  })
@@ -953,7 +1046,7 @@ const buildLevels = async ({ id, version, mode = 'full', channelRenderMode }: No
953
1046
  serviceGraphs,
954
1047
  mode,
955
1048
  domains,
956
- catalogServices: inVersionsUsed([...services, ...agents], [...domains, ...tree.map((entry) => entry.domain)], owners),
1049
+ catalogServices: inVersionsUsed([...services, ...agents], owners),
957
1050
  });
958
1051
  const context = addContext(contexts, systems, nodes);
959
1052
  const graph = { nodes: [...nodes.values(), ...context.nodes], edges: [...edges, ...context.edges] };
@@ -964,8 +1057,8 @@ const buildLevels = async ({ id, version, mode = 'full', channelRenderMode }: No
964
1057
  // by services not in one) are hidden too, with the nodes either side connected
965
1058
  // directly. A domain without systems or subdomains has no level 1.
966
1059
  if (systems.length === 0 && tree.length === 1) return layoutLevels(detailedGraph);
967
- // Built from the graph before messages only other domains send were moved
968
- // out of the systems consuming them, so they collapse into those systems
1060
+ // Built from the graph before other domains' messages were moved out of the
1061
+ // systems they were drawn in, so they collapse into those systems
969
1062
  const collapsed = collapseSystems(
970
1063
  { ...graph, edges: detailedGraph.edges },
971
1064
  systems.map(({ system }) => system),
@@ -82,79 +82,6 @@ const getRoutableNodeData = (resource: RoutableResource, mode: 'simple' | 'full'
82
82
  contextMenu: getRoutableContextMenu(resource),
83
83
  });
84
84
 
85
- const sanitizeToolId = (name: string) =>
86
- name
87
- .replace(/[^a-zA-Z0-9]+/g, '-')
88
- .replace(/(^-|-$)/g, '')
89
- .toLowerCase();
90
-
91
- const generateIdForAgentToolNode = (agent: RoutableResource, tool: { name: string; type: string }) =>
92
- `${generateIdForNode(agent)}-tool-${sanitizeToolId(`${tool.name}-${tool.type}`)}`;
93
-
94
- const appendAgentToolNodesAndEdges = ({
95
- agent,
96
- nodes,
97
- edges,
98
- mode,
99
- }: {
100
- agent: RoutableResource;
101
- nodes: Node[];
102
- edges: Edge[];
103
- mode: 'simple' | 'full';
104
- }) => {
105
- if (!isAgent(agent)) return;
106
-
107
- (agent.data.tools || []).forEach((tool) => {
108
- const toolNodeId = generateIdForAgentToolNode(agent, tool);
109
-
110
- nodes.push(
111
- createNode({
112
- id: toolNodeId,
113
- type: 'agentTool',
114
- data: {
115
- mode,
116
- agentTool: {
117
- id: sanitizeToolId(`${tool.name}-${tool.type}`),
118
- name: tool.name,
119
- type: tool.type,
120
- icon: tool.icon,
121
- url: tool.url,
122
- description: tool.description,
123
- },
124
- contextMenu: tool.url
125
- ? [
126
- {
127
- label: 'Open tool endpoint',
128
- href: tool.url,
129
- external: true,
130
- },
131
- ]
132
- : undefined,
133
- },
134
- position: { x: 0, y: 0 },
135
- })
136
- );
137
-
138
- edges.push(
139
- createEdge({
140
- id: `${generateIdForNode(agent)}-${toolNodeId}`,
141
- source: generateIdForNode(agent),
142
- target: toolNodeId,
143
- label: 'calls tool',
144
- type: 'step',
145
- animated: false,
146
- data: { animated: false },
147
- markerEnd: {
148
- type: MarkerType.ArrowClosed,
149
- color: '#666',
150
- width: 20,
151
- height: 20,
152
- },
153
- })
154
- );
155
- });
156
- };
157
-
158
85
  const getDataProductNodeData = (resource: CollectionEntry<'data-products'>, mode: 'simple' | 'full') => ({
159
86
  title: resource.data.id,
160
87
  mode,
@@ -293,9 +220,6 @@ const getNodesAndEdges = async ({
293
220
  data: isDataProduct(producer) ? getDataProductNodeData(producer, mode) : getRoutableNodeData(producer, mode),
294
221
  position: { x: 0, y: 0 },
295
222
  });
296
- if (!isDataProduct(producer)) {
297
- appendAgentToolNodesAndEdges({ agent: producer, nodes, edges, mode });
298
- }
299
223
 
300
224
  // Data products don't have channel configuration, so connect directly to the message
301
225
  if (isDataProduct(producer)) {
@@ -424,9 +348,6 @@ const getNodesAndEdges = async ({
424
348
  position: { x: 0, y: 0 },
425
349
  type: isDataProduct(consumer) ? 'data-products' : consumer.collection,
426
350
  });
427
- if (!isDataProduct(consumer)) {
428
- appendAgentToolNodesAndEdges({ agent: consumer, nodes, edges, mode });
429
- }
430
351
 
431
352
  // Data products don't have channel configuration, so connect directly from the message
432
353
  if (isDataProduct(consumer)) {
@@ -656,10 +577,6 @@ const getNodesAndEdges = async ({
656
577
  position: { x: 0, y: 0 },
657
578
  })
658
579
  );
659
-
660
- if (receiver.collection !== 'domains') {
661
- appendAgentToolNodesAndEdges({ agent: receiver, nodes, edges, mode });
662
- }
663
580
  };
664
581
 
665
582
  // Complete both sides of a trigger chain around the focused message:
@@ -779,7 +696,6 @@ export const getNodesAndEdgesForConsumedMessage = ({
779
696
  position: { x: 0, y: 0 },
780
697
  })
781
698
  );
782
- appendAgentToolNodesAndEdges({ agent: target, nodes, edges, mode });
783
699
 
784
700
  const targetMessageConfiguration = target.data.receives?.find(
785
701
  (receive) => receive.id === message.data.id && versionMatches(receive.version, message.data.version)
@@ -889,7 +805,6 @@ export const getNodesAndEdgesForConsumedMessage = ({
889
805
  position: { x: 0, y: 0 },
890
806
  })
891
807
  );
892
- appendAgentToolNodesAndEdges({ agent: producer, nodes, edges, mode });
893
808
 
894
809
  // The message is always connected directly to the producer
895
810
  edges.push(
@@ -1115,7 +1030,6 @@ export const getNodesAndEdgesForProducedMessage = ({
1115
1030
  position: { x: 0, y: 0 },
1116
1031
  })
1117
1032
  );
1118
- appendAgentToolNodesAndEdges({ agent: source, nodes, edges, mode });
1119
1033
 
1120
1034
  // Render the edge from the producer to the message
1121
1035
  edges.push(
@@ -1214,7 +1128,6 @@ export const getNodesAndEdgesForProducedMessage = ({
1214
1128
  position: { x: 0, y: 0 },
1215
1129
  })
1216
1130
  );
1217
- appendAgentToolNodesAndEdges({ agent: consumer, nodes, edges, mode });
1218
1131
 
1219
1132
  // Check if the consumer is consuming the message from a channel
1220
1133
  const consumerConfigurationForMessage = consumer.data.receives?.find(
@@ -126,21 +126,17 @@ export const getNodesAndEdges = async ({
126
126
 
127
127
  // Wrap the graph in one parent "system group" boundary node. Messages,
128
128
  // channels and data stores stay in it. With confineToMembers, only this
129
- // system's own services are children; other services (and their agent tools)
130
- // are left outside.
131
- const graphNodes = [...nodes.values()];
132
- const graphEdges = [...edges.values()];
129
+ // system's own services are children; other services are left outside.
133
130
  const graph = {
134
131
  nodes:
135
132
  wrapInSystemGroup && nodes.size > 0
136
133
  ? wrapNodesInSystemGroup(
137
- graphNodes,
138
- graphEdges,
134
+ [...nodes.values()],
139
135
  system,
140
136
  confineToMembers ? memberServiceNodeIds(system, serviceMap) : undefined
141
137
  )
142
- : graphNodes,
143
- edges: graphEdges,
138
+ : [...nodes.values()],
139
+ edges: [...edges.values()],
144
140
  };
145
141
 
146
142
  return layout ? layoutNodeGraph(graph) : graph;
@@ -171,22 +167,11 @@ const withoutGroupParent = (node: any) => {
171
167
  * Wraps a system's nodes in one parent `system-group` node (sized to fit them
172
168
  * when the graph is laid out), so they visibly belong to the system.
173
169
  * When `memberIds` is set, service and agent nodes that aren't members are
174
- * left outside the group, and so are the tools of agents left outside.
170
+ * left outside the group.
175
171
  */
176
- const wrapNodesInSystemGroup = (systemNodes: any[], systemEdges: any[], system: any, memberIds?: Set<string>) => {
172
+ const wrapNodesInSystemGroup = (systemNodes: any[], system: any, memberIds?: Set<string>) => {
177
173
  const groupId = `system-group-${system.data.id}-${system.data.version}`;
178
174
 
179
- const outside = new Set<string>(
180
- memberIds
181
- ? systemNodes.filter((node) => SERVICE_NODE_TYPES.has(node.type) && !memberIds.has(node.id)).map((node) => node.id)
182
- : []
183
- );
184
- // An agent's tools sit wherever the agent does
185
- const toolIds = new Set(systemNodes.filter((node) => node.type === 'agentTool').map((node) => node.id));
186
- for (const edge of systemEdges) {
187
- if (outside.has(edge.source) && toolIds.has(edge.target)) outside.add(edge.target);
188
- }
189
-
190
175
  const groupNode = {
191
176
  id: groupId,
192
177
  type: 'system-group',
@@ -203,8 +188,9 @@ const wrapNodesInSystemGroup = (systemNodes: any[], systemEdges: any[], system:
203
188
  // Parent must come before its children in the array for React Flow
204
189
  return [
205
190
  groupNode,
206
- ...systemNodes.map((node) =>
207
- outside.has(node.id) ? withoutGroupParent(node) : { ...node, parentId: groupId, extent: 'parent' }
208
- ),
191
+ ...systemNodes.map((node) => {
192
+ if (memberIds && SERVICE_NODE_TYPES.has(node.type) && !memberIds.has(node.id)) return withoutGroupParent(node);
193
+ return { ...node, parentId: groupId, extent: 'parent' };
194
+ }),
209
195
  ];
210
196
  };
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "BUSL-1.1",
9
9
  "type": "module",
10
- "version": "5.7.4",
10
+ "version": "5.7.5",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
@@ -141,11 +141,11 @@
141
141
  "y-protocols": "^1.0.7",
142
142
  "yjs": "^13.6.33",
143
143
  "zod": "^4.6.6",
144
- "@eventcatalog/exporters": "0.1.4",
145
144
  "@eventcatalog/fitness": "0.1.1",
145
+ "@eventcatalog/exporters": "0.1.4",
146
146
  "@eventcatalog/linter": "1.3.1",
147
- "@eventcatalog/visualiser": "^5.2.2",
148
- "@eventcatalog/sdk": "2.32.0"
147
+ "@eventcatalog/sdk": "2.32.0",
148
+ "@eventcatalog/visualiser": "^5.2.3"
149
149
  },
150
150
  "devDependencies": {
151
151
  "@astrojs/check": "^0.9.10",