@eventcatalog/core 4.1.5 → 4.2.1

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 (34) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-E6IUV4GX.js → chunk-A6SWOPJ4.js} +1 -1
  6. package/dist/{chunk-G74MXXTG.js → chunk-DTQ2MXRB.js} +1 -1
  7. package/dist/{chunk-X77TET65.js → chunk-PUTZR6SV.js} +1 -1
  8. package/dist/{chunk-GIU3S4CI.js → chunk-RQ36O7QA.js} +1 -1
  9. package/dist/{chunk-DIUAURE2.js → chunk-V6UMVNX4.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +36 -23
  13. package/dist/eventcatalog.js +41 -28
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/src/components/Lineage/LineageScenarioSwitcher.tsx +65 -0
  19. package/eventcatalog/src/content.config.ts +9 -0
  20. package/eventcatalog/src/pages/docs/[type]/[id]/[version].md.ts +3 -1
  21. package/eventcatalog/src/pages/docs/[type]/[id]/[version].mdx.ts +4 -2
  22. package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/_index.data.ts +66 -0
  23. package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/index.astro +201 -0
  24. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +31 -1
  25. package/eventcatalog/src/stores/sidebar-store/state.ts +22 -4
  26. package/eventcatalog/src/utils/collections/commands.ts +1 -1
  27. package/eventcatalog/src/utils/collections/message-triggers.ts +118 -0
  28. package/eventcatalog/src/utils/file-diffs.ts +4 -2
  29. package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +2 -1
  30. package/eventcatalog/src/utils/node-graphs/message-lineage.ts +131 -0
  31. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +119 -1
  32. package/eventcatalog/src/utils/node-graphs/services-node-graph.ts +15 -1
  33. package/eventcatalog/src/utils/node-graphs/utils/utils.ts +14 -4
  34. package/package.json +4 -4
@@ -0,0 +1,201 @@
1
+ ---
2
+ import { getCollection, type CollectionEntry } from 'astro:content';
3
+
4
+ import LineageScenarioSwitcher from '@components/Lineage/LineageScenarioSwitcher';
5
+ import AstroNodeGraph from '@components/MDX/NodeGraph/AstroNodeGraph';
6
+ import ResourceRef from '@components/MDX/ResourceRef/ResourceRef.astro';
7
+ import Footer from '@layouts/Footer.astro';
8
+ import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
9
+ import { getTriggeredByOfMessage, getTriggersOfMessage, type MessageReceiver } from '@utils/collections/message-triggers';
10
+ import { buildMessageLineageGraph, groupMessageLineageGraphs } from '@utils/node-graphs/message-lineage';
11
+ import type { CollectionMessageTypes } from '@types';
12
+
13
+ import { Page } from './_index.data';
14
+
15
+ export const prerender = Page.prerender;
16
+ export const getStaticPaths = Page.getStaticPaths;
17
+
18
+ const props = await Page.getData(Astro);
19
+ const { data } = props as CollectionEntry<CollectionMessageTypes>;
20
+ const currentMessage = props as CollectionEntry<CollectionMessageTypes>;
21
+
22
+ const [events, commands, queries, services, agents, domains] = await Promise.all([
23
+ getCollection('events'),
24
+ getCollection('commands'),
25
+ getCollection('queries'),
26
+ getCollection('services'),
27
+ getCollection('agents'),
28
+ getCollection('domains'),
29
+ ]);
30
+
31
+ const allMessages = [...events, ...commands, ...queries] as CollectionEntry<CollectionMessageTypes>[];
32
+ const receivers = [...services, ...agents, ...domains] as MessageReceiver[];
33
+
34
+ const triggeredByRows = groupMessageLineageGraphs(
35
+ getTriggeredByOfMessage(receivers, currentMessage, allMessages).map((relation) =>
36
+ buildMessageLineageGraph({ currentMessage, relation, direction: 'triggeredBy' })
37
+ )
38
+ );
39
+ const triggerRows = groupMessageLineageGraphs(
40
+ getTriggersOfMessage(receivers, currentMessage, allMessages).map((relation) =>
41
+ buildMessageLineageGraph({ currentMessage, relation, direction: 'triggers' })
42
+ )
43
+ );
44
+
45
+ const resourceTypeByCollection = {
46
+ events: 'event',
47
+ commands: 'command',
48
+ queries: 'query',
49
+ services: 'service',
50
+ domains: 'domain',
51
+ agents: 'agent',
52
+ } as const;
53
+ const resourceType = (resource: CollectionEntry<CollectionMessageTypes> | MessageReceiver) =>
54
+ resourceTypeByCollection[resource.collection];
55
+ const conditionText = (condition: string) => condition.replace(/^when\s+/i, '');
56
+ const scenarioConditions = (row: (typeof triggeredByRows)[number]) =>
57
+ row.scenarios.map(({ condition }) => (condition ? conditionText(condition) : 'No condition specified'));
58
+ ---
59
+
60
+ <VerticalSideBarLayout title={`Trigger paths | ${data.name}`} description={`Message trigger paths for ${data.name}`}>
61
+ <main class="w-full min-h-full bg-[rgb(var(--ec-page-bg))] py-8">
62
+ <header class="border-b border-[rgb(var(--ec-page-border))] py-5">
63
+ <div class="text-sm font-semibold text-[rgb(var(--ec-accent))]">Trigger paths</div>
64
+ <h1 class="pt-3 text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">{data.name}</h1>
65
+ <p class="pt-3 text-base text-[rgb(var(--ec-page-text-muted))] font-light">
66
+ Explore the direct messages that trigger this message and the messages it can trigger.
67
+ </p>
68
+ </header>
69
+
70
+ <div class="space-y-10 py-8">
71
+ {
72
+ triggeredByRows.length > 0 && (
73
+ <section aria-labelledby="triggered-by-heading">
74
+ <div class="space-y-5">
75
+ {triggeredByRows.map((row, index) => (
76
+ <article class="lineage-row overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] shadow-sm">
77
+ <div class="border-b border-[rgb(var(--ec-page-border))] px-5 py-4">
78
+ <h3 class="text-sm md:text-base font-medium text-[rgb(var(--ec-page-text))]">
79
+ <ResourceRef type={resourceType(row.target)} version={row.target.data.version}>
80
+ {row.target.data.id}
81
+ </ResourceRef>{' '}
82
+ is triggered by{' '}
83
+ <ResourceRef type={resourceType(row.source)} version={row.source.data.version}>
84
+ {row.source.data.id}
85
+ </ResourceRef>
86
+ {row.scenarios.length === 1 && row.scenarios[0].condition && (
87
+ <>
88
+ {' '}
89
+ when <span class="font-semibold">{conditionText(row.scenarios[0].condition)}</span>
90
+ </>
91
+ )}
92
+ {row.scenarios.length > 1 && (
93
+ <>
94
+ {' '}
95
+ when <LineageScenarioSwitcher client:visible conditions={scenarioConditions(row)} />
96
+ </>
97
+ )}{' '}
98
+ via{' '}
99
+ <ResourceRef type={resourceType(row.receiver)} version={row.receiver.data.version}>
100
+ {row.receiver.data.id}
101
+ </ResourceRef>
102
+ </h3>
103
+ </div>
104
+ <div
105
+ id={`${data.id}-triggered-by-${index}-portal`}
106
+ class="lineage-graph h-[250px] w-full bg-[rgb(var(--ec-page-bg))]"
107
+ />
108
+ <AstroNodeGraph
109
+ client:only="react"
110
+ id={`${data.id}-triggered-by-${index}`}
111
+ portalId={`${data.id}-triggered-by-${index}-portal`}
112
+ nodes={row.nodes}
113
+ edges={row.edges}
114
+ mode="simple"
115
+ linkTo="docs"
116
+ includeKey={false}
117
+ showSearch={false}
118
+ zoomOnScroll={false}
119
+ disableMessageAnimation={true}
120
+ />
121
+ </article>
122
+ ))}
123
+ </div>
124
+ </section>
125
+ )
126
+ }
127
+
128
+ {
129
+ triggerRows.length > 0 && (
130
+ <section aria-label="Messages triggered by this message">
131
+ <div class="space-y-5">
132
+ {triggerRows.map((row, index) => (
133
+ <article class="lineage-row overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] shadow-sm">
134
+ <div class="border-b border-[rgb(var(--ec-page-border))] px-5 py-4">
135
+ <h3 class="text-sm md:text-base font-medium text-[rgb(var(--ec-page-text))]">
136
+ <ResourceRef type={resourceType(row.target)} version={row.target.data.version}>
137
+ {row.target.data.id}
138
+ </ResourceRef>{' '}
139
+ is triggered by{' '}
140
+ <ResourceRef type={resourceType(row.source)} version={row.source.data.version}>
141
+ {row.source.data.id}
142
+ </ResourceRef>
143
+ {row.scenarios.length === 1 && row.scenarios[0].condition && (
144
+ <>
145
+ {' '}
146
+ when <span class="font-semibold">{conditionText(row.scenarios[0].condition)}</span>
147
+ </>
148
+ )}
149
+ {row.scenarios.length > 1 && (
150
+ <>
151
+ {' '}
152
+ when <LineageScenarioSwitcher client:visible conditions={scenarioConditions(row)} />
153
+ </>
154
+ )}{' '}
155
+ via{' '}
156
+ <ResourceRef type={resourceType(row.receiver)} version={row.receiver.data.version}>
157
+ {row.receiver.data.id}
158
+ </ResourceRef>
159
+ </h3>
160
+ </div>
161
+ <div
162
+ id={`${data.id}-triggers-${index}-portal`}
163
+ class="lineage-graph h-[250px] w-full bg-[rgb(var(--ec-page-bg))]"
164
+ />
165
+ <AstroNodeGraph
166
+ client:only="react"
167
+ id={`${data.id}-triggers-${index}`}
168
+ portalId={`${data.id}-triggers-${index}-portal`}
169
+ nodes={row.nodes}
170
+ edges={row.edges}
171
+ mode="simple"
172
+ linkTo="docs"
173
+ includeKey={false}
174
+ showSearch={false}
175
+ zoomOnScroll={false}
176
+ disableMessageAnimation={true}
177
+ />
178
+ </article>
179
+ ))}
180
+ </div>
181
+ </section>
182
+ )
183
+ }
184
+ </div>
185
+
186
+ <Footer />
187
+ </main>
188
+ </VerticalSideBarLayout>
189
+
190
+ <style is:global>
191
+ .lineage-row {
192
+ content-visibility: auto;
193
+ contain-intrinsic-size: 330px;
194
+ }
195
+
196
+ .lineage-graph .react-flow__panel.top,
197
+ .lineage-graph .react-flow__controls,
198
+ .lineage-graph .react-flow__attribution {
199
+ display: none;
200
+ }
201
+ </style>
@@ -14,6 +14,7 @@ import {
14
14
  import { isVisualiserEnabled, isChangelogEnabled } from '@utils/feature';
15
15
  import { iconFieldsForResource } from '@utils/icon';
16
16
  import { collectionToResourceMap } from '@utils/collections/util';
17
+ import type { MessageTrigger } from '@utils/collections/message-triggers';
17
18
 
18
19
  type MessageSchemaEntry = CollectionEntry<'schemas'>;
19
20
 
@@ -47,16 +48,22 @@ export const buildMessageNode = (
47
48
  owners: any[],
48
49
  context: ResourceGroupContext,
49
50
  hasFieldUsage: boolean = false,
50
- flowRefs: string[] = []
51
+ flowRefs: string[] = [],
52
+ messageTriggers: { triggers: MessageTrigger[]; triggeredBy: MessageTrigger[] } = { triggers: [], triggeredBy: [] }
51
53
  ): NavNode => {
52
54
  const producers = message.data.producers || [];
53
55
  const consumers = message.data.consumers || [];
54
56
  const collection = message.collection;
57
+ const triggerRefs = [...new Set(messageTriggers.triggers.map(({ message }) => getProducerConsumerPageRef(message)))];
58
+ const triggeredByRefs = [...new Set(messageTriggers.triggeredBy.map(({ message }) => getProducerConsumerPageRef(message)))];
55
59
 
56
60
  const renderProducers = producers.length > 0 && shouldRenderSideBarSection(message, 'producers');
57
61
  const renderConsumers = consumers.length > 0 && shouldRenderSideBarSection(message, 'consumers');
62
+ const renderTriggers = triggerRefs.length > 0 && shouldRenderSideBarSection(message, 'triggers');
63
+ const renderTriggeredBy = triggeredByRefs.length > 0 && shouldRenderSideBarSection(message, 'triggeredBy');
58
64
  const renderFlows = flowRefs.length > 0 && shouldRenderSideBarSection(message, 'flows');
59
65
  const renderRepository = message.data.repository && shouldRenderSideBarSection(message, 'repository');
66
+ const hasTriggerPaths = messageTriggers.triggers.length > 0 || messageTriggers.triggeredBy.length > 0;
60
67
 
61
68
  // Determine badge based on collection type
62
69
  const badgeMap: Record<string, string> = {
@@ -124,6 +131,15 @@ export const buildMessageNode = (
124
131
  title: 'Map',
125
132
  href: buildUrl(`/visualiser/${collection}/${message.data.id}/${message.data.version}`),
126
133
  },
134
+ ...(hasTriggerPaths
135
+ ? [
136
+ {
137
+ type: 'item' as const,
138
+ title: 'Trigger paths',
139
+ href: buildUrl(`/triggers/${collection}/${message.data.id}/${message.data.version}`),
140
+ },
141
+ ]
142
+ : []),
127
143
  ],
128
144
  },
129
145
  hasDiagrams && {
@@ -163,6 +179,20 @@ export const buildMessageNode = (
163
179
  pages: consumers.map(getProducerConsumerPageRef),
164
180
  visible: consumers.length > 0,
165
181
  },
182
+ renderTriggeredBy && {
183
+ type: 'group',
184
+ title: 'Triggered by',
185
+ icon: 'Mail',
186
+ pages: triggeredByRefs,
187
+ visible: triggeredByRefs.length > 0,
188
+ },
189
+ renderTriggers && {
190
+ type: 'group',
191
+ title: 'Triggers',
192
+ icon: 'Mail',
193
+ pages: triggerRefs,
194
+ visible: triggerRefs.length > 0,
195
+ },
166
196
  renderFlows && {
167
197
  type: 'group',
168
198
  title: 'Appears in flows',
@@ -8,6 +8,7 @@ import { getDomains } from '@utils/collections/domains';
8
8
  import { getSystems } from '@utils/collections/systems';
9
9
  import { getServices } from '@utils/collections/services';
10
10
  import { getMessages, pluralizeMessageType } from '@utils/collections/messages';
11
+ import { getTriggeredByOfMessage, getTriggersOfMessage } from '@utils/collections/message-triggers';
11
12
  import { getOwner } from '@utils/collections/owners';
12
13
  import { getFlows } from '@utils/collections/flows';
13
14
  import { getUsers } from '@utils/collections/users';
@@ -451,10 +452,14 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
451
452
  {} as Record<string, NavNode | string>
452
453
  );
453
454
 
454
- const rawAgents = await getCollection('agents');
455
+ const [rawAgents, rawServices, rawDomains] = await Promise.all([
456
+ getCollection('agents'),
457
+ getCollection('services'),
458
+ getCollection('domains'),
459
+ ]);
460
+ const messageReceivers = [...rawServices, ...rawAgents, ...rawDomains];
455
461
 
456
462
  // Compute channels for each service from raw sends[].to / receives[].from pointers
457
- const rawServices = await getCollection('services');
458
463
  const channelMap = createVersionedMap(channels);
459
464
  const serviceChannelsMap = new Map<string, CollectionEntry<'channels'>[]>();
460
465
 
@@ -560,7 +565,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
560
565
  {} as Record<string, NavNode | string>
561
566
  );
562
567
 
563
- // Build a set of message IDs that have field usage declared by any service or agent.
568
+ // Build a set of message IDs that have field usage declared by any resource that can receive messages.
564
569
  // We use raw collections because the hydrated resources replace
565
570
  // sends/receives pointers with resolved message entries, which strips the fields property.
566
571
  const messagesWithFieldUsage = new Set<string>();
@@ -580,6 +585,14 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
580
585
  if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
581
586
  }
582
587
  }
588
+ for (const domain of rawDomains) {
589
+ for (const pointer of domain.data.sends || []) {
590
+ if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
591
+ }
592
+ for (const pointer of domain.data.receives || []) {
593
+ if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
594
+ }
595
+ }
583
596
 
584
597
  const flowRefsByMessage = buildFlowReferencesByMessage({ flows, events, commands, queries });
585
598
 
@@ -588,8 +601,13 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
588
601
  const type = pluralizeMessageType(message as any);
589
602
  const versionedKey = `${type}:${message.data.id}:${message.data.version}`;
590
603
  const hasFieldUsage = messagesWithFieldUsage.has(message.data.id);
604
+ const triggers = getTriggersOfMessage(messageReceivers, message, messages);
605
+ const triggeredBy = getTriggeredByOfMessage(messageReceivers, message, messages);
591
606
  acc[versionedKey] = withArchitectureDecisionsSection(
592
- buildMessageNode(message, owners, context, hasFieldUsage, flowRefsByMessage.get(versionedKey) || []),
607
+ buildMessageNode(message, owners, context, hasFieldUsage, flowRefsByMessage.get(versionedKey) || [], {
608
+ triggers,
609
+ triggeredBy,
610
+ }),
593
611
  message,
594
612
  adrs
595
613
  );
@@ -20,7 +20,7 @@ export const getCommands = async ({ getAllVersions = true, hydrateServices = tru
20
20
  const cacheKey = `${getAllVersions ? 'allVersions' : 'currentVersions'}-${hydrateServices ? 'hydrated' : 'minimal'}`;
21
21
 
22
22
  // Check cache
23
- if (memoryCache[cacheKey] && memoryCache[cacheKey].length > 0) {
23
+ if (memoryCache[cacheKey] && memoryCache[cacheKey].length > 0 && CACHE_ENABLED) {
24
24
  // console.timeEnd('✅ New getCommands');
25
25
  return memoryCache[cacheKey];
26
26
  }
@@ -0,0 +1,118 @@
1
+ import type { CollectionEntry } from 'astro:content';
2
+ import type { CollectionMessageTypes } from '@types';
3
+ import { getItemsFromCollectionByIdAndSemverOrLatest, versionMatches } from './util';
4
+
5
+ type Message = CollectionEntry<CollectionMessageTypes>;
6
+ export type MessageReceiver = CollectionEntry<'services'> | CollectionEntry<'domains'> | CollectionEntry<'agents'>;
7
+
8
+ interface TriggerPointer {
9
+ id: string;
10
+ version?: string;
11
+ condition?: string;
12
+ }
13
+
14
+ interface ReceivePointer {
15
+ id: string;
16
+ version?: string;
17
+ triggers?: TriggerPointer[];
18
+ }
19
+
20
+ export interface MessageTrigger {
21
+ receiver: MessageReceiver;
22
+ message: Message;
23
+ condition?: string;
24
+ }
25
+
26
+ const messageMatchesPointer = (message: Message, pointer: { id: string; version?: string }) => {
27
+ if (pointer.id !== message.data.id) return false;
28
+
29
+ if (!pointer.version || pointer.version === 'latest') {
30
+ return !message.data.latestVersion || message.data.version === message.data.latestVersion;
31
+ }
32
+
33
+ return versionMatches(message.data.version, pointer.version);
34
+ };
35
+
36
+ /**
37
+ * Returns the messages triggered when a resource receives the given message.
38
+ *
39
+ * Receivers must be raw collection entries because hydration replaces receive
40
+ * pointers with messages and therefore removes trigger metadata.
41
+ */
42
+ export const getTriggersOfMessage = (
43
+ receivers: MessageReceiver[],
44
+ message: Message,
45
+ allMessages: Message[]
46
+ ): MessageTrigger[] => {
47
+ const triggers: MessageTrigger[] = [];
48
+
49
+ for (const receiver of receivers) {
50
+ const receives = (receiver.data.receives as ReceivePointer[]) ?? [];
51
+
52
+ for (const receive of receives) {
53
+ if (!receive.triggers?.length || !messageMatchesPointer(message, receive)) continue;
54
+
55
+ for (const trigger of receive.triggers) {
56
+ const target = getItemsFromCollectionByIdAndSemverOrLatest(allMessages, trigger.id, trigger.version)[0];
57
+ if (!target) continue;
58
+
59
+ triggers.push({ receiver, message: target, condition: trigger.condition });
60
+ }
61
+ }
62
+ }
63
+
64
+ return triggers;
65
+ };
66
+
67
+ /**
68
+ * Returns the messages whose handling can trigger the given message.
69
+ *
70
+ * Receivers must be raw collection entries because hydration replaces receive
71
+ * pointers with messages and therefore removes trigger metadata.
72
+ */
73
+ export const getTriggeredByOfMessage = (
74
+ receivers: MessageReceiver[],
75
+ message: Message,
76
+ allMessages: Message[]
77
+ ): MessageTrigger[] => {
78
+ const triggeredBy: MessageTrigger[] = [];
79
+
80
+ for (const receiver of receivers) {
81
+ const receives = (receiver.data.receives as ReceivePointer[]) ?? [];
82
+
83
+ for (const receive of receives) {
84
+ for (const trigger of receive.triggers ?? []) {
85
+ if (!messageMatchesPointer(message, trigger)) continue;
86
+
87
+ const source = getItemsFromCollectionByIdAndSemverOrLatest(allMessages, receive.id, receive.version)[0];
88
+ if (!source) continue;
89
+
90
+ triggeredBy.push({ receiver, message: source, condition: trigger.condition });
91
+ }
92
+ }
93
+ }
94
+
95
+ return triggeredBy;
96
+ };
97
+
98
+ const getMessageKey = (message: Message) => `${message.collection}:${message.data.id}:${message.data.version}`;
99
+
100
+ /**
101
+ * Returns only messages that participate in at least one resolved trigger path.
102
+ * Both the source and target are included so empty trigger pages are never generated.
103
+ */
104
+ export const getMessagesWithTriggerPaths = (receivers: MessageReceiver[], allMessages: Message[]): Message[] => {
105
+ const messageKeysWithPaths = new Set<string>();
106
+
107
+ for (const source of allMessages) {
108
+ const triggers = getTriggersOfMessage(receivers, source, allMessages);
109
+ if (triggers.length === 0) continue;
110
+
111
+ messageKeysWithPaths.add(getMessageKey(source));
112
+ for (const trigger of triggers) {
113
+ messageKeysWithPaths.add(getMessageKey(trigger.message));
114
+ }
115
+ }
116
+
117
+ return allMessages.filter((message) => messageKeysWithPaths.has(getMessageKey(message)));
118
+ };
@@ -1,8 +1,9 @@
1
1
  import { readdir, readFile } from 'node:fs/promises';
2
- import { dirname, join } from 'node:path';
2
+ import { dirname, isAbsolute, join } from 'node:path';
3
3
  import { formatPatch, structuredPatch } from 'diff';
4
4
  import { html, parse } from 'diff2html';
5
5
  import { getItemsFromCollectionByIdAndSemverOrLatest } from './collections/util';
6
+ import { getAbsoluteFilePathForAstroFile } from './files';
6
7
  import type { CollectionEntry } from 'astro:content';
7
8
  import type { CollectionTypes } from '@types';
8
9
 
@@ -32,7 +33,8 @@ export async function getFilesForDiffInCollection(
32
33
  const pathToFolder = collection.filePath;
33
34
  if (!pathToFolder) return [];
34
35
 
35
- const dir = dirname(pathToFolder);
36
+ const absolutePathToFolder = isAbsolute(pathToFolder) ? pathToFolder : getAbsoluteFilePathForAstroFile(pathToFolder);
37
+ const dir = dirname(absolutePathToFolder);
36
38
  const allFilesInDirectory = await readdir(dir);
37
39
 
38
40
  return allFilesInDirectory
@@ -143,11 +143,12 @@ export const getNodesAndEdges = async ({
143
143
  targetPosition: 'left',
144
144
  data: {
145
145
  mode,
146
+ isFocused: true,
146
147
  data: {
147
148
  ...container.data,
148
149
  },
149
150
  contextMenu: buildContextMenuForResource({
150
- collection: 'entities',
151
+ collection: 'containers',
151
152
  id: container.data.id,
152
153
  version: container.data.version,
153
154
  }),
@@ -0,0 +1,131 @@
1
+ import type { CollectionEntry } from 'astro:content';
2
+ import type { Edge, Node } from '@xyflow/react';
3
+ import type { CollectionMessageTypes } from '@types';
4
+ import type { MessageTrigger } from '@utils/collections/message-triggers';
5
+ import {
6
+ createEdge,
7
+ createNode,
8
+ generateIdForNode,
9
+ generatedIdForEdge,
10
+ getColorFromString,
11
+ getEdgeLabelForMessageAsSource,
12
+ getEdgeLabelForServiceAsTarget,
13
+ } from './utils/utils';
14
+ import { createTriggerMessageNode, getTriggerReceiverNodeData } from './message-node-graph';
15
+
16
+ type Message = CollectionEntry<CollectionMessageTypes>;
17
+
18
+ export type MessageLineageDirection = 'triggeredBy' | 'triggers';
19
+
20
+ export interface MessageLineageGraph {
21
+ direction: MessageLineageDirection;
22
+ condition?: string;
23
+ source: Message;
24
+ receiver: MessageTrigger['receiver'];
25
+ target: Message;
26
+ relatedMessage: Message;
27
+ nodes: Node[];
28
+ edges: Edge[];
29
+ }
30
+
31
+ export interface MessageLineageScenario {
32
+ condition?: string;
33
+ }
34
+
35
+ export interface MessageLineageGroup extends Omit<MessageLineageGraph, 'condition'> {
36
+ scenarios: MessageLineageScenario[];
37
+ }
38
+
39
+ const createLineageEdge = (source: any, target: any, label: string, colorMessage: Message): Edge =>
40
+ createEdge({
41
+ id: generatedIdForEdge(source, target),
42
+ source: generateIdForNode(source),
43
+ target: generateIdForNode(target),
44
+ label,
45
+ animated: false,
46
+ data: {
47
+ animated: false,
48
+ customColor: getColorFromString(colorMessage.data.id),
49
+ rootSourceAndTarget: {
50
+ source: { id: generateIdForNode(source), collection: source.collection },
51
+ target: { id: generateIdForNode(target), collection: target.collection },
52
+ },
53
+ },
54
+ });
55
+
56
+ export const buildMessageLineageGraph = ({
57
+ currentMessage,
58
+ relation,
59
+ direction,
60
+ }: {
61
+ currentMessage: Message;
62
+ relation: MessageTrigger;
63
+ direction: MessageLineageDirection;
64
+ }): MessageLineageGraph => {
65
+ const source = direction === 'triggeredBy' ? relation.message : currentMessage;
66
+ const target = direction === 'triggeredBy' ? currentMessage : relation.message;
67
+ const mode = 'simple';
68
+
69
+ const nodes: Node[] = [
70
+ {
71
+ ...createTriggerMessageNode(source, mode, source === currentMessage),
72
+ position: { x: 0, y: 0 },
73
+ },
74
+ createNode({
75
+ id: generateIdForNode(relation.receiver),
76
+ type: relation.receiver.collection,
77
+ data: getTriggerReceiverNodeData(relation.receiver, mode),
78
+ position: { x: 320, y: 0 },
79
+ }),
80
+ {
81
+ ...createTriggerMessageNode(target, mode, target === currentMessage),
82
+ position: { x: 640, y: 0 },
83
+ },
84
+ ];
85
+
86
+ return {
87
+ direction,
88
+ condition: relation.condition,
89
+ source,
90
+ receiver: relation.receiver,
91
+ target,
92
+ relatedMessage: relation.message,
93
+ nodes,
94
+ edges: [
95
+ createLineageEdge(source, relation.receiver, getEdgeLabelForMessageAsSource(source), source),
96
+ createLineageEdge(relation.receiver, target, getEdgeLabelForServiceAsTarget(target), target),
97
+ ],
98
+ };
99
+ };
100
+
101
+ const getResourceKey = (resource: Message | MessageTrigger['receiver']) =>
102
+ `${resource.collection}:${resource.data.id}:${resource.data.version}`;
103
+
104
+ export const groupMessageLineageGraphs = (graphs: MessageLineageGraph[]): MessageLineageGroup[] => {
105
+ const groups = new Map<string, MessageLineageGroup>();
106
+
107
+ for (const graph of graphs) {
108
+ const key = [
109
+ graph.direction,
110
+ getResourceKey(graph.source),
111
+ getResourceKey(graph.receiver),
112
+ getResourceKey(graph.target),
113
+ ].join('>');
114
+ const existingGroup = groups.get(key);
115
+
116
+ if (existingGroup) {
117
+ if (!existingGroup.scenarios.some((scenario) => scenario.condition === graph.condition)) {
118
+ existingGroup.scenarios.push({ condition: graph.condition });
119
+ }
120
+ continue;
121
+ }
122
+
123
+ const { condition, ...lineage } = graph;
124
+ groups.set(key, {
125
+ ...lineage,
126
+ scenarios: [{ condition }],
127
+ });
128
+ }
129
+
130
+ return [...groups.values()];
131
+ };