@eventcatalog/core 4.10.8 → 4.10.10

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 (44) 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-W6IMDOJJ.js → chunk-K45ABG6N.js} +1 -1
  6. package/dist/{chunk-LB5EMDAY.js → chunk-PR77FB54.js} +1 -1
  7. package/dist/{chunk-CDWE6WWB.js → chunk-QXANW76J.js} +1 -1
  8. package/dist/{chunk-W5C4K7F2.js → chunk-U6R6BFJ5.js} +1 -1
  9. package/dist/{chunk-DRK5QSQC.js → chunk-Y6VX2CGL.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  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/MDX/NodeGraph/NodeGraph.astro +6 -2
  19. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +58 -12
  20. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +5 -5
  21. package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +2 -0
  22. package/eventcatalog/src/components/SchemaExplorer/types.ts +10 -1
  23. package/eventcatalog/src/components/SchemaExplorer/useSchemaDetails.ts +69 -0
  24. package/eventcatalog/src/components/SchemaExplorer/utils.ts +12 -0
  25. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.spec.ts +52 -0
  26. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.ts +20 -11
  27. package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +3 -269
  28. package/eventcatalog/src/pages/schemas/explorer/content/[key].json.ts +17 -0
  29. package/eventcatalog/src/stores/sidebar-store/state.ts +3 -28
  30. package/eventcatalog/src/utils/collections/field-usage.ts +48 -0
  31. package/eventcatalog/src/utils/node-graphs/channel-node-graph.ts +2 -2
  32. package/eventcatalog/src/utils/node-graphs/compact-visualiser-graph.ts +246 -0
  33. package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +2 -2
  34. package/eventcatalog/src/utils/node-graphs/data-products-node-graph.ts +2 -2
  35. package/eventcatalog/src/utils/node-graphs/domains-canvas.ts +2 -2
  36. package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +2 -2
  37. package/eventcatalog/src/utils/node-graphs/flows-node-graph.ts +2 -2
  38. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +2 -2
  39. package/eventcatalog/src/utils/node-graphs/services-node-graph.ts +2 -2
  40. package/eventcatalog/src/utils/node-graphs/system-context-node-graph.ts +2 -2
  41. package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +8 -3
  42. package/eventcatalog/src/utils/node-graphs/utils/utils.ts +30 -2
  43. package/eventcatalog/src/utils/schema-explorer.ts +276 -0
  44. package/package.json +3 -3
@@ -1,272 +1,6 @@
1
1
  import { isSSR } from '@utils/feature';
2
2
  import { HybridPage } from '@utils/page-loaders/hybrid-page';
3
- import { getEvents } from '@utils/collections/events';
4
- import { getCommands } from '@utils/collections/commands';
5
- import { getQueries } from '@utils/collections/queries';
6
- import { getServices, getSpecificationsForService } from '@utils/collections/services';
7
- import { getDomains, getSpecificationsForDomain } from '@utils/collections/domains';
8
- import { getDataProducts } from '@utils/collections/data-products';
9
- import { getOwner } from '@utils/collections/owners';
10
- import { buildUrl } from '@utils/url-builder';
11
- import { resourceFileExists, readResourceFile } from '@utils/resource-files';
12
- import { getExamplesForResource } from '@utils/collections/examples';
13
- import { getCollection } from 'astro:content';
14
- import path from 'path';
15
-
16
- // Helper function to enrich owners with full details
17
- async function enrichOwners(ownersRaw: any[]) {
18
- if (!ownersRaw || ownersRaw.length === 0) return [];
19
-
20
- const owners = await Promise.all(ownersRaw.map(getOwner));
21
- const filteredOwners = owners.filter((o) => o !== undefined);
22
-
23
- return filteredOwners.map((o) => ({
24
- id: o.data.id,
25
- name: o.data.name,
26
- type: o.collection,
27
- href: buildUrl(`/docs/${o.collection}/${o.data.id}`),
28
- }));
29
- }
30
-
31
- async function fetchAllSchemas() {
32
- // Fetch all messages
33
- const events = await getEvents({ getAllVersions: true });
34
- const commands = await getCommands({ getAllVersions: true });
35
- const queries = await getQueries({ getAllVersions: true });
36
- const schemaEntries = await getCollection('schemas');
37
-
38
- // Fetch all services
39
- const services = await getServices({ getAllVersions: true });
40
-
41
- // Combine all messages
42
- const allMessages = [...events, ...commands, ...queries];
43
- const messagesBySchemaReference = new Map(
44
- allMessages.map((message) => [`${message.collection}:${message.data.id}:${message.data.version}`, message])
45
- );
46
-
47
- // Read message schemas from the generated schemas collection.
48
- const messagesWithSchemas = await Promise.all(
49
- schemaEntries.map(async (schema) => {
50
- const message = messagesBySchemaReference.get(
51
- `${schema.data.message.collectionName}:${schema.data.message.id}:${schema.data.message.version}`
52
- );
53
- const schemaPath = schema.data.file || schema.data.source.path || '';
54
- const schemaExtension = path.extname(schemaPath).slice(1) || schema.data.format;
55
-
56
- if (message) {
57
- try {
58
- const enrichedOwners = await enrichOwners(schema.data.message.owners || []);
59
-
60
- return {
61
- collection: message.collection,
62
- data: {
63
- id: message.data.id,
64
- name: schema.data.message.name || message.data.name,
65
- version: message.data.version,
66
- summary: schema.data.message.summary || message.data.summary,
67
- schemaPath,
68
- producers: message.data.producers || [],
69
- consumers: message.data.consumers || [],
70
- owners: enrichedOwners,
71
- },
72
- schemaContent: schema.data.content || '',
73
- schemaExtension,
74
- examples: getExamplesForResource(message),
75
- };
76
- } catch (error) {
77
- console.error(`Error reading schema metadata for ${message.data.id}:`, error);
78
- const enrichedOwners = await enrichOwners(schema.data.message.owners || []);
79
- return {
80
- collection: message.collection,
81
- data: {
82
- id: message.data.id,
83
- name: schema.data.message.name || schema.data.name || message.data.name,
84
- version: message.data.version,
85
- summary: schema.data.message.summary || message.data.summary,
86
- schemaPath,
87
- producers: message.data.producers || [],
88
- consumers: message.data.consumers || [],
89
- owners: enrichedOwners,
90
- },
91
- schemaContent: schema.data.content || '',
92
- schemaExtension,
93
- };
94
- }
95
- }
96
-
97
- return {
98
- collection: schema.data.message.collectionName,
99
- data: {
100
- id: schema.data.message.id,
101
- name: schema.data.message.name || schema.data.name || schema.data.message.id,
102
- version: schema.data.message.version,
103
- summary: schema.data.message.summary,
104
- schemaPath,
105
- owners: await enrichOwners(schema.data.message.owners || []),
106
- producers: [],
107
- consumers: [],
108
- },
109
- schemaContent: schema.data.content || '',
110
- schemaExtension,
111
- examples: [],
112
- };
113
- })
114
- );
115
-
116
- // Filter services with specifications and read spec content - only keep essential data
117
- const servicesWithSpecs = await Promise.all(
118
- services.map(async (service) => {
119
- try {
120
- const specifications = getSpecificationsForService(service);
121
-
122
- if (specifications.length === 0) {
123
- return null;
124
- }
125
-
126
- return await Promise.all(
127
- specifications.map(async (spec) => {
128
- if (!resourceFileExists(service, spec.path)) {
129
- return null;
130
- }
131
-
132
- const schemaContent = readResourceFile(service, spec.path) ?? '';
133
- const schemaExtension = spec.type;
134
- const enrichedOwners = await enrichOwners(service.data.owners || []);
135
-
136
- return {
137
- collection: 'services',
138
- data: {
139
- id: `${service.data.id}`,
140
- name: `${service.data.name} - ${spec.name}`,
141
- version: service.data.version,
142
- summary: service.data.summary,
143
- schemaPath: spec.path,
144
- owners: enrichedOwners,
145
- },
146
- schemaContent,
147
- schemaExtension,
148
- specType: spec.type,
149
- specName: spec.name,
150
- specFilenameWithoutExtension: spec.filenameWithoutExtension,
151
- };
152
- })
153
- );
154
- } catch (error) {
155
- console.error(`Error reading specifications for service ${service.data.id}:`, error);
156
- return null;
157
- }
158
- })
159
- );
160
-
161
- // Flatten and filter out null values
162
- const flatServicesWithSpecs = servicesWithSpecs.flat().filter((service) => service !== null);
163
-
164
- // Fetch all domains
165
- const domains = await getDomains({ getAllVersions: true });
166
-
167
- // Filter domains with specifications and read spec content - only keep essential data
168
- const domainsWithSpecs = await Promise.all(
169
- domains.map(async (domain) => {
170
- try {
171
- const specifications = getSpecificationsForDomain(domain);
172
-
173
- if (specifications.length === 0) {
174
- return null;
175
- }
176
-
177
- return await Promise.all(
178
- specifications.map(async (spec) => {
179
- if (!resourceFileExists(domain, spec.path)) {
180
- return null;
181
- }
182
-
183
- const schemaContent = readResourceFile(domain, spec.path) ?? '';
184
- const schemaExtension = spec.type;
185
- const enrichedOwners = await enrichOwners(domain.data.owners || []);
186
-
187
- return {
188
- collection: 'domains',
189
- data: {
190
- id: `${domain.data.id}`,
191
- name: `${domain.data.name} - ${spec.name}`,
192
- version: domain.data.version,
193
- summary: domain.data.summary,
194
- schemaPath: spec.path,
195
- owners: enrichedOwners,
196
- },
197
- schemaContent,
198
- schemaExtension,
199
- specType: spec.type,
200
- specName: spec.name,
201
- specFilenameWithoutExtension: spec.filenameWithoutExtension,
202
- };
203
- })
204
- );
205
- } catch (error) {
206
- console.error(`Error reading specifications for domain ${domain.data.id}:`, error);
207
- return null;
208
- }
209
- })
210
- );
211
-
212
- // Flatten and filter out null values for domains
213
- const flatDomainsWithSpecs = domainsWithSpecs.flat().filter((domain) => domain !== null);
214
-
215
- // Fetch all data products and extract contracts from outputs
216
- const dataProducts = await getDataProducts({ getAllVersions: true });
217
-
218
- // Filter data products with contracts in outputs and read contract content
219
- const dataProductsWithContracts = await Promise.all(
220
- dataProducts.map(async (dataProduct) => {
221
- try {
222
- const outputs = dataProduct.data.outputs || [];
223
- const outputsWithContracts = outputs.filter((output) => output.contract);
224
-
225
- if (outputsWithContracts.length === 0) {
226
- return null;
227
- }
228
-
229
- return await Promise.all(
230
- outputsWithContracts.map(async (output) => {
231
- const contract = output.contract!;
232
- if (!resourceFileExists(dataProduct, contract.path)) {
233
- return null;
234
- }
235
-
236
- const schemaContent = readResourceFile(dataProduct, contract.path) ?? '';
237
- const schemaExtension = path.extname(contract.path).slice(1) || 'json';
238
- const enrichedOwners = await enrichOwners(dataProduct.data.owners || []);
239
-
240
- return {
241
- collection: 'data-products',
242
- data: {
243
- id: `${dataProduct.data.id}__${contract.path}`,
244
- name: contract.name,
245
- version: dataProduct.data.version,
246
- summary: `Data contract for ${dataProduct.data.name}`,
247
- schemaPath: contract.path,
248
- owners: enrichedOwners,
249
- },
250
- schemaContent,
251
- schemaExtension,
252
- contractType: contract.type,
253
- dataProductId: dataProduct.data.id,
254
- dataProductVersion: dataProduct.data.version,
255
- };
256
- })
257
- );
258
- } catch (error) {
259
- console.error(`Error reading contracts for data product ${dataProduct.data.id}:`, error);
260
- return null;
261
- }
262
- })
263
- );
264
-
265
- // Flatten and filter out null values for data product contracts
266
- const flatDataProductContracts = dataProductsWithContracts.flat().filter((contract) => contract !== null);
267
-
268
- return [...messagesWithSchemas, ...flatServicesWithSpecs, ...flatDomainsWithSpecs, ...flatDataProductContracts];
269
- }
3
+ import { getSchemaMetadata } from '@utils/schema-explorer';
270
4
 
271
5
  export class Page extends HybridPage {
272
6
  static get prerender(): boolean {
@@ -278,7 +12,7 @@ export class Page extends HybridPage {
278
12
  return [];
279
13
  }
280
14
 
281
- const allSchemas = await fetchAllSchemas();
15
+ const allSchemas = await getSchemaMetadata();
282
16
 
283
17
  return [
284
18
  {
@@ -291,7 +25,7 @@ export class Page extends HybridPage {
291
25
  }
292
26
 
293
27
  protected static async fetchData(_params: any) {
294
- const allSchemas = await fetchAllSchemas();
28
+ const allSchemas = await getSchemaMetadata();
295
29
  return {
296
30
  schemas: allSchemas,
297
31
  };
@@ -0,0 +1,17 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { isSSR } from '@utils/feature';
3
+ import { getSchemaRegistry, getSchemaDetails } from '@utils/schema-explorer';
4
+
5
+ export const prerender = !isSSR();
6
+
7
+ export async function getStaticPaths() {
8
+ return [...(await getSchemaRegistry()).keys()].map((key) => ({ params: { key } }));
9
+ }
10
+
11
+ export const GET: APIRoute = async ({ params }) => {
12
+ const details = params.key ? await getSchemaDetails(params.key) : undefined;
13
+ if (!details) return new Response('Schema not found', { status: 404 });
14
+ return new Response(JSON.stringify(details), {
15
+ headers: { 'Content-Type': 'application/json' },
16
+ });
17
+ };
@@ -42,6 +42,7 @@ import {
42
42
  withArchitectureDecisionsSection,
43
43
  } from './builders/shared';
44
44
  import { isArchitectureGraphEnabled, isChangelogEnabled, isResourceDocsEnabled } from '@utils/feature';
45
+ import { collectMessageIdsWithFieldUsage } from '@utils/collections/field-usage';
45
46
 
46
47
  export type { NavigationData, NavNode, ChildRef };
47
48
 
@@ -580,34 +581,8 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
580
581
  {} as Record<string, NavNode | string>
581
582
  );
582
583
 
583
- // Build a set of message IDs that have field usage declared by any resource that can receive messages.
584
- // We use raw collections because the hydrated resources replace
585
- // sends/receives pointers with resolved message entries, which strips the fields property.
586
- const messagesWithFieldUsage = new Set<string>();
587
- for (const agent of rawAgents) {
588
- for (const pointer of agent.data.sends || []) {
589
- if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
590
- }
591
- for (const pointer of agent.data.receives || []) {
592
- if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
593
- }
594
- }
595
- for (const service of rawServices) {
596
- for (const pointer of service.data.sends || []) {
597
- if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
598
- }
599
- for (const pointer of service.data.receives || []) {
600
- if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
601
- }
602
- }
603
- for (const domain of rawDomains) {
604
- for (const pointer of domain.data.sends || []) {
605
- if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
606
- }
607
- for (const pointer of domain.data.receives || []) {
608
- if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
609
- }
610
- }
584
+ // Raw collections keep send/receive pointers (and their `fields`) which hydration strips.
585
+ const messagesWithFieldUsage = collectMessageIdsWithFieldUsage(rawAgents, rawServices, rawDomains);
611
586
 
612
587
  const flowRefsByMessage = buildFlowReferencesByMessage({
613
588
  flows,
@@ -0,0 +1,48 @@
1
+ import { getCollection } from 'astro:content';
2
+
3
+ type PointerWithFields = { id?: string; fields?: string[] };
4
+
5
+ type ResourceWithMessagePointers = {
6
+ data: {
7
+ sends?: PointerWithFields[];
8
+ receives?: PointerWithFields[];
9
+ };
10
+ };
11
+
12
+ const addFieldUsageFromPointers = (messageIds: Set<string>, pointers: PointerWithFields[] | undefined) => {
13
+ for (const pointer of pointers || []) {
14
+ if (pointer.id && pointer.fields?.length) {
15
+ messageIds.add(pointer.id);
16
+ }
17
+ }
18
+ };
19
+
20
+ /**
21
+ * Message ids that have field-level lineage declared on a send/receive pointer.
22
+ * Uses raw (unhydrated) resources because hydration replaces those pointers and drops `fields`.
23
+ * Matches the sidebar "Field Usage" link gate.
24
+ */
25
+ export const collectMessageIdsWithFieldUsage = (
26
+ agents: ResourceWithMessagePointers[],
27
+ services: ResourceWithMessagePointers[],
28
+ domains: ResourceWithMessagePointers[]
29
+ ): Set<string> => {
30
+ const messageIds = new Set<string>();
31
+
32
+ for (const resource of [...agents, ...services, ...domains]) {
33
+ addFieldUsageFromPointers(messageIds, resource.data.sends);
34
+ addFieldUsageFromPointers(messageIds, resource.data.receives);
35
+ }
36
+
37
+ return messageIds;
38
+ };
39
+
40
+ export const getMessageIdsWithFieldUsage = async (): Promise<Set<string>> => {
41
+ const [agents, services, domains] = await Promise.all([
42
+ getCollection('agents'),
43
+ getCollection('services'),
44
+ getCollection('domains'),
45
+ ]);
46
+
47
+ return collectMessageIdsWithFieldUsage(agents, services, domains);
48
+ };
@@ -1,5 +1,4 @@
1
1
  import { getCollection, type CollectionEntry } from 'astro:content';
2
- import dagre from 'dagre';
3
2
  import {
4
3
  buildContextMenuForAgent,
5
4
  buildContextMenuForMessage,
@@ -17,6 +16,7 @@ import {
17
16
  getOperationFields,
18
17
  DEFAULT_NODE_WIDTH,
19
18
  DEFAULT_NODE_HEIGHT,
19
+ layoutDagreGraph,
20
20
  } from './utils/utils';
21
21
  import { createVersionedMap, findInMap } from '@utils/collections/util';
22
22
  import { getChannelChain, getChannels } from '@utils/collections/channels';
@@ -457,7 +457,7 @@ export const getNodesAndEdges = async ({ id, version, defaultFlow, mode = 'simpl
457
457
  });
458
458
 
459
459
  if (layout) {
460
- dagre.layout(flow);
460
+ layoutDagreGraph(flow);
461
461
  }
462
462
 
463
463
  return {
@@ -0,0 +1,246 @@
1
+ import type { Edge, Node } from '@xyflow/react';
2
+
3
+ /**
4
+ * Fields the visualiser nodes actually read from a resource. Everything else
5
+ * (markdown bodies, file paths, hydrated producers/consumers, full send/receive
6
+ * collections) is dropped before Astro serialises the graph into the page.
7
+ */
8
+ const RESOURCE_DATA_KEYS = [
9
+ 'id',
10
+ 'name',
11
+ 'version',
12
+ 'summary',
13
+ 'owners',
14
+ 'deprecated',
15
+ 'draft',
16
+ 'notes',
17
+ 'styles',
18
+ 'badges',
19
+ 'latestVersion',
20
+ 'method',
21
+ 'path',
22
+ 'statusCodes',
23
+ 'specifications',
24
+ 'externalSystem',
25
+ 'repository',
26
+ 'model',
27
+ 'tools',
28
+ 'parameters',
29
+ 'protocols',
30
+ 'address',
31
+ 'deliveryGuarantee',
32
+ 'type',
33
+ 'container_type',
34
+ 'schemas',
35
+ 'properties',
36
+ 'aggregateRoot',
37
+ 'sidebar',
38
+ ] as const;
39
+
40
+ const RESOURCE_OBJECT_KEYS = new Set([
41
+ 'message',
42
+ 'service',
43
+ 'agent',
44
+ 'domain',
45
+ 'channel',
46
+ 'dataProduct',
47
+ 'container',
48
+ 'entity',
49
+ 'flow',
50
+ 'data',
51
+ ]);
52
+
53
+ const DROP_EDGE_RESOURCE_KEYS = new Set(['publisherService', 'consumerService']);
54
+
55
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
56
+ typeof value === 'object' && value !== null && !Array.isArray(value);
57
+
58
+ const isCollectionEntry = (value: Record<string, unknown>): boolean =>
59
+ isPlainObject(value.data) &&
60
+ ('collection' in value || 'filePath' in value || 'digest' in value || 'body' in value || 'rendered' in value);
61
+
62
+ const compactDomainService = (service: unknown) => {
63
+ if (!isPlainObject(service)) return service;
64
+
65
+ const data = isPlainObject(service.data) ? service.data : service;
66
+ const id = data.id ?? service.id;
67
+ const name = data.name ?? service.name;
68
+ const version = data.version ?? service.version;
69
+
70
+ return {
71
+ data: {
72
+ ...(id !== undefined ? { id } : {}),
73
+ ...(name !== undefined ? { name } : {}),
74
+ ...(version !== undefined ? { version } : {}),
75
+ },
76
+ };
77
+ };
78
+
79
+ const compactOwners = (owners: unknown): unknown => {
80
+ if (!Array.isArray(owners)) return owners;
81
+ return owners.map((owner) => {
82
+ if (typeof owner === 'string') return owner;
83
+ if (!isPlainObject(owner)) return owner;
84
+ const data = isPlainObject(owner.data) ? owner.data : owner;
85
+ const id = data.id ?? owner.id;
86
+ const name = data.name ?? owner.name;
87
+ if (id === undefined && name === undefined) return owner;
88
+ return {
89
+ ...(id !== undefined ? { id } : {}),
90
+ ...(name !== undefined ? { name } : {}),
91
+ };
92
+ });
93
+ };
94
+
95
+ export const compactVisualiserResource = (value: unknown): unknown => {
96
+ if (value == null || typeof value !== 'object') return value;
97
+ if (Array.isArray(value)) return value.map(compactVisualiserResource);
98
+
99
+ const obj = value as Record<string, unknown>;
100
+
101
+ if (isCollectionEntry(obj)) {
102
+ const data = compactVisualiserResourceData(obj.data);
103
+ const resourceId = isPlainObject(data) && data.id !== undefined ? data.id : obj.id;
104
+ return {
105
+ ...(resourceId !== undefined ? { id: resourceId } : {}),
106
+ ...(obj.collection !== undefined ? { collection: obj.collection } : {}),
107
+ data,
108
+ // EventNode / ServiceNode / ChannelNode read name/version/summary from the
109
+ // top level. Collection-entry shaped payloads only have those on `.data`.
110
+ ...(isPlainObject(data) ? data : {}),
111
+ };
112
+ }
113
+
114
+ return compactVisualiserResourceData(obj);
115
+ };
116
+
117
+ const compactVisualiserResourceData = (value: unknown): unknown => {
118
+ if (!isPlainObject(value)) return value;
119
+
120
+ const out: Record<string, unknown> = {};
121
+ for (const key of RESOURCE_DATA_KEYS) {
122
+ if (value[key] === undefined) continue;
123
+ if (key === 'owners') {
124
+ out[key] = compactOwners(value[key]);
125
+ continue;
126
+ }
127
+ if (key === 'tools' && Array.isArray(value[key])) {
128
+ out[key] = value[key];
129
+ continue;
130
+ }
131
+ out[key] = value[key];
132
+ }
133
+
134
+ // Domain nodes render `domain.data.services[].data.{id,name,version}`.
135
+ if (Array.isArray(value.services)) {
136
+ out.services = value.services.map(compactDomainService);
137
+ }
138
+
139
+ return out;
140
+ };
141
+
142
+ const compactEndpoint = (endpoint: unknown): unknown => {
143
+ if (!isPlainObject(endpoint)) return endpoint;
144
+
145
+ const data = isPlainObject(endpoint.data) ? endpoint.data : undefined;
146
+ const collection = endpoint.collection ?? data?.collection;
147
+ const id = endpoint.id ?? (data?.id !== undefined && data?.version !== undefined ? `${data.id}-${data.version}` : data?.id);
148
+
149
+ return {
150
+ ...(id !== undefined ? { id } : {}),
151
+ ...(collection !== undefined ? { collection } : {}),
152
+ ...(data
153
+ ? {
154
+ data: {
155
+ ...(data.id !== undefined ? { id: data.id } : {}),
156
+ ...(data.version !== undefined ? { version: data.version } : {}),
157
+ ...(collection !== undefined ? { collection } : {}),
158
+ },
159
+ }
160
+ : {}),
161
+ };
162
+ };
163
+
164
+ const compactNodeData = (data: unknown): unknown => {
165
+ if (!isPlainObject(data)) return data;
166
+
167
+ const out: Record<string, unknown> = {};
168
+
169
+ for (const [key, value] of Object.entries(data)) {
170
+ if (value === undefined) continue;
171
+
172
+ if (RESOURCE_OBJECT_KEYS.has(key)) {
173
+ out[key] = compactVisualiserResource(value);
174
+ continue;
175
+ }
176
+
177
+ if (key === 'messages' && Array.isArray(value)) {
178
+ out[key] = value.map((entry) => {
179
+ if (!isPlainObject(entry)) return entry;
180
+ return {
181
+ ...entry,
182
+ ...(entry.message !== undefined ? { message: compactVisualiserResource(entry.message) } : {}),
183
+ ...(Array.isArray(entry.channels) ? { channels: entry.channels.map(compactVisualiserResource) } : {}),
184
+ };
185
+ });
186
+ continue;
187
+ }
188
+
189
+ out[key] = value;
190
+ }
191
+
192
+ return out;
193
+ };
194
+
195
+ const compactEdgeData = (data: unknown): unknown => {
196
+ if (!isPlainObject(data)) return data;
197
+
198
+ const out: Record<string, unknown> = {};
199
+
200
+ for (const [key, value] of Object.entries(data)) {
201
+ if (value === undefined || DROP_EDGE_RESOURCE_KEYS.has(key)) continue;
202
+
203
+ if (key === 'rootSourceAndTarget' && isPlainObject(value)) {
204
+ out[key] = {
205
+ source: compactEndpoint(value.source),
206
+ target: compactEndpoint(value.target),
207
+ };
208
+ continue;
209
+ }
210
+
211
+ if (RESOURCE_OBJECT_KEYS.has(key) || key === 'message') {
212
+ out[key] = compactVisualiserResource(value);
213
+ continue;
214
+ }
215
+
216
+ out[key] = value;
217
+ }
218
+
219
+ return out;
220
+ };
221
+
222
+ const compactNode = <T extends Node>(node: T): T => {
223
+ const { data, ...rest } = node;
224
+ return {
225
+ ...rest,
226
+ ...(data !== undefined ? { data: compactNodeData(data) } : {}),
227
+ } as T;
228
+ };
229
+
230
+ const compactEdge = <T extends Edge>(edge: T): T => {
231
+ const { data, ...rest } = edge;
232
+ return {
233
+ ...rest,
234
+ ...(data !== undefined ? { data: compactEdgeData(data) } : {}),
235
+ } as T;
236
+ };
237
+
238
+ /**
239
+ * Strip collection-entry weight from a React Flow graph before it is serialised
240
+ * into an Astro island. Node/edge structure and the fields visualiser components
241
+ * read are preserved.
242
+ */
243
+ export const compactVisualiserGraph = <N extends Node, E extends Edge>(nodes: N[], edges: E[]): { nodes: N[]; edges: E[] } => ({
244
+ nodes: nodes.map(compactNode),
245
+ edges: edges.map(compactEdge),
246
+ });
@@ -1,5 +1,4 @@
1
1
  import type { CollectionEntry } from 'astro:content';
2
- import dagre from 'dagre';
3
2
  import {
4
3
  calculatedNodes,
5
4
  createDagreGraph,
@@ -10,6 +9,7 @@ import {
10
9
  buildContextMenuForResource,
11
10
  DEFAULT_NODE_WIDTH,
12
11
  DEFAULT_NODE_HEIGHT,
12
+ layoutDagreGraph,
13
13
  } from './utils/utils';
14
14
  import { MarkerType } from '@xyflow/react';
15
15
  import { findMatchingNodes } from '@utils/collections/util';
@@ -299,7 +299,7 @@ export const getNodesAndEdges = async ({
299
299
 
300
300
  // Render the diagram in memory getting hte X and Y
301
301
  if (layout) {
302
- dagre.layout(flow);
302
+ layoutDagreGraph(flow);
303
303
  }
304
304
 
305
305
  return {