@eventcatalog/core 5.4.2 → 5.5.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.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/catalog-runtime.cjs +1 -1
- package/dist/{chunk-VSBMYXLL.js → chunk-6KR5ZLEQ.js} +1 -1
- package/dist/{chunk-XB6FJMI3.js → chunk-EBZQJP7F.js} +1 -1
- package/dist/{chunk-HV4ONKTM.js → chunk-GPIKYBQ5.js} +1 -1
- package/dist/{chunk-EXRIIYEP.js → chunk-GZLVDQPM.js} +1 -1
- package/dist/{chunk-CQ4L4SMB.js → chunk-K5QKWVI3.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +2 -2
- package/dist/eventcatalog.js +14 -14
- package/dist/federation/federate.js +3 -3
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +2 -0
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -0
- package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +5 -3
- package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +6 -5
- package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.tsx +6 -5
- package/eventcatalog/src/features/ai/chat-api.ts +6 -3
- package/eventcatalog/src/features/mcp/apps/architecture-diagram/shared.ts +3 -1
- package/eventcatalog/src/features/mcp/apps/architecture-diagram/view.tsx +6 -1
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +80 -68
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +87 -73
- package/eventcatalog/src/features/mcp/mcp-apps.ts +1 -1
- package/eventcatalog/src/features/mcp/mcp-scoped-tools.ts +2 -1
- package/eventcatalog/src/features/mcp/mcp-server.ts +14 -12
- package/eventcatalog/src/features/mcp/schema-viewer.ts +3 -3
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +1 -1
- package/eventcatalog/src/features/tools/catalog-tools.ts +143 -75
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/_mermaid-route.ts +48 -5
- package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/[level].mermaid.ts +18 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/system-context-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/[level].mermaid.ts +18 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/utils/node-graphs/architecture-diagram-types.ts +7 -1
- package/eventcatalog/src/utils/node-graphs/architecture-diagram.ts +5 -2
- package/eventcatalog/src/utils/node-graphs/compact-visualiser-graph.ts +2 -0
- package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +246 -29
- package/package.json +5 -5
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -375
- package/dist/{chunk-QJ7KKN2D.js → chunk-NCEZDQSW.js} +7 -7
|
@@ -93,7 +93,7 @@ export function registerMcpApp<Shape extends z.ZodRawShape, ViewShape extends z.
|
|
|
93
93
|
if ('error' in result) return errorResult(result);
|
|
94
94
|
return {
|
|
95
95
|
// The model reads the text; hosts that support MCP Apps show the view from `_meta`
|
|
96
|
-
content: [{ type: 'text' as const, text: JSON.stringify(result.forModel
|
|
96
|
+
content: [{ type: 'text' as const, text: JSON.stringify(result.forModel) }],
|
|
97
97
|
...(loadView && {
|
|
98
98
|
_meta: {
|
|
99
99
|
[app.metaKey]: await result.getPayload(),
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
paginate,
|
|
13
13
|
} from '@features/tools/catalog-tools';
|
|
14
14
|
import type { McpScope } from './mcp-scope';
|
|
15
|
+
import type { ArchitectureDiagramDetail } from '@utils/node-graphs/architecture-diagram-types';
|
|
15
16
|
|
|
16
17
|
const notFound = (collection: string, id: string, version?: string) => ({
|
|
17
18
|
error: `Resource not found: ${collection}/${id}${version ? ` (${version})` : ''}`,
|
|
@@ -186,7 +187,7 @@ export function createScopedCatalogTools(scope: McpScope) {
|
|
|
186
187
|
resourceId: string;
|
|
187
188
|
resourceVersion?: string;
|
|
188
189
|
resourceCollection: string;
|
|
189
|
-
detail?:
|
|
190
|
+
detail?: ArchitectureDiagramDetail;
|
|
190
191
|
}) {
|
|
191
192
|
const entry = scope.find(params.resourceCollection, params.resourceId, params.resourceVersion);
|
|
192
193
|
if (!entry) return notFound(params.resourceCollection, params.resourceId, params.resourceVersion);
|
|
@@ -11,6 +11,7 @@ import { McpScopeNotFoundError, resolveMcpScope, type McpScope, type McpScopeKin
|
|
|
11
11
|
import { getArchitectureDiagramView } from '@utils/node-graphs/architecture-diagram';
|
|
12
12
|
import type {
|
|
13
13
|
ArchitectureDiagramCollection,
|
|
14
|
+
ArchitectureDiagramDetail,
|
|
14
15
|
ArchitectureDiagramView,
|
|
15
16
|
Graph,
|
|
16
17
|
} from '@utils/node-graphs/architecture-diagram-types';
|
|
@@ -30,7 +31,7 @@ const loadViewerView = getMcpAppViewLoader('viewer');
|
|
|
30
31
|
|
|
31
32
|
const SHOW_RESOURCE_DESCRIPTION = [
|
|
32
33
|
"Show the user a resource in EventCatalog's interactive viewer. Pick what to show with view:",
|
|
33
|
-
'"architecture" shows an architecture diagram of a domain, system, service, agent, event, command, query, flow, data store or data product: what it connects to, what it publishes and consumes, and the systems and domains around it. Use it whenever the user wants to see or understand how something works or fits together, e.g. "show me how the Order Service works", "how does the Ordering domain fit together", "what does Payments talk to", or asks for a diagram, map or visual of something. Prefer it over drawing your own diagram.
|
|
34
|
+
'"architecture" shows an architecture diagram of a domain, system, service, agent, event, command, query, flow, data store or data product: what it connects to, what it publishes and consumes, and the systems and domains around it. Use it whenever the user wants to see or understand how something works or fits together, e.g. "show me how the Order Service works", "how does the Ordering domain fit together", "what does Payments talk to", or asks for a diagram, map or visual of something. Prefer it over drawing your own diagram. Pick the level with detail: "overview" for just the domains, systems and their relationships (domains and systems), "services" for services and data stores without messages and channels, or "full" (default) to include messages and channels.',
|
|
34
35
|
'"schema" shows the schema of a message (event, command or query) or other resource: its properties, types, descriptions and which ones are required. Use it whenever the user wants to see or understand a schema or payload, e.g. "show me the schema for OrderCreated", "what fields does PlaceOrder have", "what does the OrderShipped payload look like".',
|
|
35
36
|
'Clients that support MCP Apps show one viewer and switch it to each thing you show, so call this again to show the user something else. The result also contains what is shown (the diagram as Mermaid in mermaidCode, or the schema code) so you can understand it.',
|
|
36
37
|
'When the client shows the viewer, do not redraw the diagram or print the whole schema again in your reply: explain it and point out what matters. Otherwise, show the user the mermaidCode or the relevant parts of the schema code.',
|
|
@@ -47,7 +48,8 @@ type McpServerOptions = {
|
|
|
47
48
|
|
|
48
49
|
const catalogDirectory = process.env.PROJECT_DIR || process.cwd();
|
|
49
50
|
|
|
50
|
-
// Helper to create consistent MCP tool handlers with error handling
|
|
51
|
+
// Helper to create consistent MCP tool handlers with error handling. Results are compact JSON: indenting them costs
|
|
52
|
+
// the model about a fifth more tokens for nothing
|
|
51
53
|
function createToolHandler<T>(fn: (params: T) => Promise<any>, errorMessage: string) {
|
|
52
54
|
return async (params: T) => {
|
|
53
55
|
try {
|
|
@@ -61,7 +63,7 @@ function createToolHandler<T>(fn: (params: T) => Promise<any>, errorMessage: str
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
return {
|
|
64
|
-
content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
66
|
+
content: [{ type: 'text' as const, text: JSON.stringify(result) }],
|
|
65
67
|
};
|
|
66
68
|
} catch (error) {
|
|
67
69
|
return {
|
|
@@ -389,6 +391,7 @@ function createMcpServer(
|
|
|
389
391
|
resourceId: string;
|
|
390
392
|
resourceVersion: string;
|
|
391
393
|
visualiserUrl: string;
|
|
394
|
+
detail?: ArchitectureDiagramDetail;
|
|
392
395
|
}): Promise<ArchitectureDiagramPayload> => ({
|
|
393
396
|
resource: {
|
|
394
397
|
collection: diagram.resourceCollection,
|
|
@@ -405,6 +408,7 @@ function createMcpServer(
|
|
|
405
408
|
version: diagram.resourceVersion,
|
|
406
409
|
})
|
|
407
410
|
),
|
|
411
|
+
...(diagram.detail ? { detail: diagram.detail } : {}),
|
|
408
412
|
});
|
|
409
413
|
|
|
410
414
|
// The resource's schemas, parsed for the view's JSON Schema, Avro and Protobuf viewers
|
|
@@ -455,7 +459,7 @@ function createMcpServer(
|
|
|
455
459
|
resourceId: string;
|
|
456
460
|
resourceVersion?: string;
|
|
457
461
|
resourceCollection: string;
|
|
458
|
-
detail?:
|
|
462
|
+
detail?: ArchitectureDiagramDetail;
|
|
459
463
|
};
|
|
460
464
|
|
|
461
465
|
const loadArchitectureView = async ({ view, ...params }: ResourceViewParams) => {
|
|
@@ -470,7 +474,8 @@ function createMcpServer(
|
|
|
470
474
|
label: `the architecture diagram for ${result.resourceCollection}/${result.resourceId}`,
|
|
471
475
|
getPayload: async (): Promise<ViewerPayload> => ({
|
|
472
476
|
view: 'architecture',
|
|
473
|
-
|
|
477
|
+
// Opens at the level asked for, as the Mermaid the model gets is; without one, at the diagram's default
|
|
478
|
+
diagram: await getArchitectureDiagramPayload({ ...result, detail: params.detail }),
|
|
474
479
|
}),
|
|
475
480
|
};
|
|
476
481
|
};
|
|
@@ -517,12 +522,9 @@ function createMcpServer(
|
|
|
517
522
|
description: SHOW_RESOURCE_DESCRIPTION,
|
|
518
523
|
input: {
|
|
519
524
|
...resourceViewInput,
|
|
520
|
-
detail:
|
|
521
|
-
.enum(['overview', 'full'])
|
|
525
|
+
detail: catalogTools.architectureDiagramDetailSchema
|
|
522
526
|
.optional()
|
|
523
|
-
.describe(
|
|
524
|
-
'For view "architecture" of domains and systems: "overview" shows only domains, systems and their relationships; "full" (default) also shows services, messages and channels'
|
|
525
|
-
),
|
|
527
|
+
.describe(`For view "architecture": ${catalogTools.architectureDiagramDetailSchema.description}`),
|
|
526
528
|
},
|
|
527
529
|
},
|
|
528
530
|
viewTool: {
|
|
@@ -664,7 +666,7 @@ function createMcpServer(
|
|
|
664
666
|
try {
|
|
665
667
|
const result = await execute(params);
|
|
666
668
|
return {
|
|
667
|
-
content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
669
|
+
content: [{ type: 'text' as const, text: JSON.stringify(result) }],
|
|
668
670
|
};
|
|
669
671
|
} catch (error) {
|
|
670
672
|
return {
|
|
@@ -839,7 +841,7 @@ function createMcpServer(
|
|
|
839
841
|
contents: [
|
|
840
842
|
{
|
|
841
843
|
uri: uri.href,
|
|
842
|
-
text: JSON.stringify({ ...(scope && { scope: scope.ref }), resources: allResources }
|
|
844
|
+
text: JSON.stringify({ ...(scope && { scope: scope.ref }), resources: allResources }),
|
|
843
845
|
mimeType: 'application/json',
|
|
844
846
|
},
|
|
845
847
|
],
|
|
@@ -3,7 +3,7 @@ import { getLanguageForHighlight } from '@components/SchemaExplorer/utils';
|
|
|
3
3
|
import { getSchemaExtensionForFormat } from '@utils/collections/schemas';
|
|
4
4
|
import type { SchemaViewerSchema } from './apps/schema-viewer/shared';
|
|
5
5
|
|
|
6
|
-
type SchemaToolResult = { name?: string; format?: string;
|
|
6
|
+
type SchemaToolResult = { name?: string; format?: string; path?: string; code: string };
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* The schemas returned by getSchemaForResource, ready for the schema viewer: parsed for its Properties tab
|
|
@@ -12,11 +12,11 @@ type SchemaToolResult = { name?: string; format?: string; url?: string; code: st
|
|
|
12
12
|
export const toSchemaViewerSchemas = (schemas: SchemaToolResult[]): SchemaViewerSchema[] =>
|
|
13
13
|
schemas.map((schema) => {
|
|
14
14
|
// Schemas in the schemas collection have a format (jsonschema, avro, protobuf), others a file extension
|
|
15
|
-
const format = schema.format ?? schema.
|
|
15
|
+
const format = schema.format ?? schema.path?.split('.').pop() ?? '';
|
|
16
16
|
const extension = getSchemaExtensionForFormat(format);
|
|
17
17
|
return {
|
|
18
18
|
...parseSchemaForViewer(extension, schema.code),
|
|
19
|
-
...(schemas.length > 1 && { name: schema.name ?? schema.
|
|
19
|
+
...(schemas.length > 1 && { name: schema.name ?? schema.path?.split('/').pop() ?? format }),
|
|
20
20
|
format,
|
|
21
21
|
language: getLanguageForHighlight(extension),
|
|
22
22
|
code: schema.code,
|
|
@@ -97,6 +97,7 @@ import {
|
|
|
97
97
|
type HotspotFields,
|
|
98
98
|
type HotspotSeverity,
|
|
99
99
|
type HotspotSpot,
|
|
100
|
+
RemeasureHandles,
|
|
100
101
|
} from '@eventcatalog/visualiser/studio';
|
|
101
102
|
import { X } from 'lucide-react';
|
|
102
103
|
import type { CatalogRelation, CatalogResource } from '../catalog-resources';
|
|
@@ -1751,6 +1752,7 @@ function Canvas({
|
|
|
1751
1752
|
maxZoom={2}
|
|
1752
1753
|
>
|
|
1753
1754
|
<DiagramBackground />
|
|
1755
|
+
<RemeasureHandles />
|
|
1754
1756
|
{/* Above the selected node's top right corner: its details, to edit them */}
|
|
1755
1757
|
{toEdit && <EditButton nodeId={toEdit.id} locked={locked} onClick={editSelected} />}
|
|
1756
1758
|
<LegendPanel legend={legend} hiddenKeys={hiddenLegendKeys} onLegendClick={toggleLegendKey} />
|
|
@@ -52,7 +52,7 @@ const CANVAS_ICON_SVG =
|
|
|
52
52
|
const CANVAS_ICON = { src: `data:image/svg+xml;base64,${btoa(CANVAS_ICON_SVG)}`, mimeType: 'image/svg+xml', sizes: ['20x20'] };
|
|
53
53
|
|
|
54
54
|
const text = (value: unknown, isError = false) => ({
|
|
55
|
-
content: [{ type: 'text' as const, text: typeof value === 'string' ? value : JSON.stringify(value
|
|
55
|
+
content: [{ type: 'text' as const, text: typeof value === 'string' ? value : JSON.stringify(value) }],
|
|
56
56
|
...(isError && { isError }),
|
|
57
57
|
});
|
|
58
58
|
|
|
@@ -4,18 +4,19 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getCollection } from 'astro:content';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
-
import {
|
|
8
|
-
import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util';
|
|
7
|
+
import { getSchemaFormatFromURL } from '@utils/collections/schemas';
|
|
8
|
+
import { getItemsFromCollectionByIdAndSemverOrLatest, processSpecifications, versionMatches } from '@utils/collections/util';
|
|
9
9
|
import { getUbiquitousLanguageWithSubdomains } from '@utils/collections/domains';
|
|
10
10
|
import { getAbsoluteFilePathForAstroFile } from '@utils/files';
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import { getArchitectureDiagramGraph } from '@utils/node-graphs/architecture-diagram';
|
|
13
13
|
import {
|
|
14
14
|
ARCHITECTURE_DIAGRAM_COLLECTIONS,
|
|
15
|
+
ARCHITECTURE_DIAGRAM_DETAILS,
|
|
15
16
|
isArchitectureDiagramCollection,
|
|
16
17
|
type ArchitectureDiagramDetail,
|
|
17
18
|
} from '@utils/node-graphs/architecture-diagram-types';
|
|
18
|
-
import { convertToMermaid } from '@
|
|
19
|
+
import { convertToMermaid } from '@eventcatalog/visualiser/layout';
|
|
19
20
|
import { glob } from 'glob';
|
|
20
21
|
import path from 'node:path';
|
|
21
22
|
import {
|
|
@@ -201,6 +202,12 @@ export const resourceCollectionSchema = z.enum([
|
|
|
201
202
|
|
|
202
203
|
export const visualiserCollectionSchema = z.enum(ARCHITECTURE_DIAGRAM_COLLECTIONS);
|
|
203
204
|
|
|
205
|
+
export const architectureDiagramDetailSchema = z
|
|
206
|
+
.enum(ARCHITECTURE_DIAGRAM_DETAILS)
|
|
207
|
+
.describe(
|
|
208
|
+
'The level of the diagram, as on its Diagram page: "overview" (L1) shows only domains, systems and their relationships (domains and systems only); "services" (L2) shows services and data stores without messages and channels; "full" (L3, default) also shows messages and channels'
|
|
209
|
+
);
|
|
210
|
+
|
|
204
211
|
// ============================================
|
|
205
212
|
// Tool implementations (core logic)
|
|
206
213
|
// ============================================
|
|
@@ -273,6 +280,74 @@ export async function getResource(params: { collection: string; id: string; vers
|
|
|
273
280
|
|
|
274
281
|
type MessagePointer = { id: string; version?: string };
|
|
275
282
|
|
|
283
|
+
type MessageEntry = { collection: string; data: { id: string; version: string } };
|
|
284
|
+
|
|
285
|
+
/** Resources that send and receive messages, and the collections that hold them */
|
|
286
|
+
const MESSAGING_COLLECTIONS = ['services', 'agents', 'domains'] as const;
|
|
287
|
+
|
|
288
|
+
/** Old versions live in a versioned folder: what they send and receive is history, not the catalog today */
|
|
289
|
+
const isLatestVersion = (entry: { filePath?: string }) => !entry.filePath?.includes('versioned');
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Whether a sends/receives pointer points at this version of a message, as on the message's pages: one without a
|
|
293
|
+
* version (or "latest") points at the latest version only; others match exactly, by semver range or by x-pattern (1.x)
|
|
294
|
+
*/
|
|
295
|
+
const pointsAtMessage = (pointer: MessagePointer, message: MessageEntry, latestVersion: string) => {
|
|
296
|
+
if (pointer.id !== message.data.id) return false;
|
|
297
|
+
if (!pointer.version || pointer.version === 'latest') return message.data.version === latestVersion;
|
|
298
|
+
return versionMatches(message.data.version, pointer.version);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/** The services, agents and domains (latest versions) that send (or receive) this version of a message */
|
|
302
|
+
const getResourcesThat = async (direction: 'sends' | 'receives', message: MessageEntry) => {
|
|
303
|
+
const [latestMessage, ...collections] = await Promise.all([
|
|
304
|
+
findEntry(message.collection, message.data.id),
|
|
305
|
+
...MESSAGING_COLLECTIONS.map((collection) => getCollection(collection)),
|
|
306
|
+
]);
|
|
307
|
+
const latestVersion = latestMessage?.data.version ?? message.data.version;
|
|
308
|
+
|
|
309
|
+
return collections
|
|
310
|
+
.flat()
|
|
311
|
+
.filter(isLatestVersion)
|
|
312
|
+
.filter((resource) =>
|
|
313
|
+
((resource.data as any)[direction] || []).some((pointer: MessagePointer) =>
|
|
314
|
+
pointsAtMessage(pointer, message, latestVersion)
|
|
315
|
+
)
|
|
316
|
+
);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The resources a domain or system is made of, with it: its services and agents, and for a domain its systems and
|
|
321
|
+
* subdomains (and everything inside them)
|
|
322
|
+
*/
|
|
323
|
+
const getResourcesInside = async (container: any) => {
|
|
324
|
+
const [domains, systems, services, agents] = await Promise.all(
|
|
325
|
+
(['domains', 'systems', 'services', 'agents'] as const).map((collection) => getCollection(collection))
|
|
326
|
+
);
|
|
327
|
+
const resolve = (entries: any[], pointers: MessagePointer[] = []) =>
|
|
328
|
+
pointers
|
|
329
|
+
.map((pointer) => getItemsFromCollectionByIdAndSemverOrLatest(entries, pointer.id, pointer.version)[0])
|
|
330
|
+
.filter(Boolean);
|
|
331
|
+
|
|
332
|
+
const found = new Map<string, any>();
|
|
333
|
+
const visit = (entry: any) => {
|
|
334
|
+
const key = `${entry.collection}/${entry.data.id}`;
|
|
335
|
+
if (found.has(key)) return;
|
|
336
|
+
found.set(key, entry);
|
|
337
|
+
if (entry.collection !== 'domains' && entry.collection !== 'systems') return;
|
|
338
|
+
[
|
|
339
|
+
...resolve(services, entry.data.services),
|
|
340
|
+
...resolve(agents, entry.data.agents),
|
|
341
|
+
...(entry.collection === 'domains'
|
|
342
|
+
? [...resolve(systems, entry.data.systems), ...resolve(domains, entry.data.domains)]
|
|
343
|
+
: []),
|
|
344
|
+
].forEach(visit);
|
|
345
|
+
};
|
|
346
|
+
visit(container);
|
|
347
|
+
|
|
348
|
+
return [...found.values()];
|
|
349
|
+
};
|
|
350
|
+
|
|
276
351
|
/**
|
|
277
352
|
* Resolve a sends/receives pointer to the documented message it points at
|
|
278
353
|
*/
|
|
@@ -314,17 +389,39 @@ export async function getMessagesProducedOrConsumedByResource(params: {
|
|
|
314
389
|
getCollection('queries'),
|
|
315
390
|
]);
|
|
316
391
|
const messages = [...events, ...commands, ...queries];
|
|
392
|
+
const summary = {
|
|
393
|
+
id: resource.data.id,
|
|
394
|
+
version: resource.data.version,
|
|
395
|
+
name: resource.data.name || resource.data.id,
|
|
396
|
+
collection: params.resourceCollection,
|
|
397
|
+
};
|
|
317
398
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
399
|
+
if (params.resourceCollection !== 'domains' && params.resourceCollection !== 'systems') {
|
|
400
|
+
return {
|
|
401
|
+
resource: summary,
|
|
402
|
+
sends: (resource.data.sends || []).map((pointer: MessagePointer) => resolveMessagePointer(messages, pointer)),
|
|
403
|
+
receives: (resource.data.receives || []).map((pointer: MessagePointer) => resolveMessagePointer(messages, pointer)),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// A domain or system sends and receives what it and everything inside it does: `by` names who, e.g. services/orders
|
|
408
|
+
const members = await getResourcesInside(resource);
|
|
409
|
+
const collect = (direction: 'sends' | 'receives') => {
|
|
410
|
+
const found = new Map<string, ReturnType<typeof resolveMessagePointer> & { by: string[] }>();
|
|
411
|
+
for (const member of members) {
|
|
412
|
+
for (const pointer of (member.data[direction] || []) as MessagePointer[]) {
|
|
413
|
+
const message = resolveMessagePointer(messages, pointer);
|
|
414
|
+
const key = `${message.collection ?? ''}/${message.id}@${message.version}`;
|
|
415
|
+
const entry = found.get(key) ?? { ...message, by: [] };
|
|
416
|
+
const by = `${member.collection}/${member.data.id}`;
|
|
417
|
+
if (!entry.by.includes(by)) entry.by.push(by);
|
|
418
|
+
found.set(key, entry);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return [...found.values()];
|
|
327
422
|
};
|
|
423
|
+
|
|
424
|
+
return { resource: summary, sends: collect('sends'), receives: collect('receives') };
|
|
328
425
|
}
|
|
329
426
|
|
|
330
427
|
/**
|
|
@@ -360,15 +457,27 @@ export async function getSchemaForResource(params: { resourceId: string; resourc
|
|
|
360
457
|
}
|
|
361
458
|
}
|
|
362
459
|
|
|
363
|
-
const
|
|
460
|
+
const files = [
|
|
461
|
+
...(resource.data.schemaPath
|
|
462
|
+
? [{ path: resource.data.schemaPath, format: getSchemaFormatFromURL(resource.data.schemaPath) }]
|
|
463
|
+
: []),
|
|
464
|
+
...processSpecifications(resource.data.specifications).map((spec) => ({
|
|
465
|
+
name: spec.name,
|
|
466
|
+
path: spec.path,
|
|
467
|
+
format: spec.type,
|
|
468
|
+
})),
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
// Read from beside the resource's own file (paths are relative to it); files that can't be read are left out
|
|
472
|
+
const schemas = files.flatMap((file) => {
|
|
473
|
+
try {
|
|
474
|
+
return [{ ...file, code: fs.readFileSync(getAbsoluteFilePathForAstroFile(resource.filePath ?? '', file.path), 'utf-8') }];
|
|
475
|
+
} catch {
|
|
476
|
+
return [];
|
|
477
|
+
}
|
|
478
|
+
});
|
|
364
479
|
|
|
365
|
-
if (
|
|
366
|
-
return schema.map((schemaItem) => ({
|
|
367
|
-
url: schemaItem.url,
|
|
368
|
-
format: schemaItem.format,
|
|
369
|
-
code: fs.readFileSync(schemaItem.url, 'utf-8'),
|
|
370
|
-
}));
|
|
371
|
-
}
|
|
480
|
+
if (schemas.length > 0) return schemas;
|
|
372
481
|
|
|
373
482
|
return { message: 'No schemas found for this resource' };
|
|
374
483
|
}
|
|
@@ -424,8 +533,6 @@ export async function findResourcesByOwner(params: { ownerId: string }) {
|
|
|
424
533
|
* Get resources that produce (send) a specific message
|
|
425
534
|
*/
|
|
426
535
|
export async function getProducersOfMessage(params: { messageId: string; messageVersion?: string; messageCollection: string }) {
|
|
427
|
-
const [services, agents] = await Promise.all([getCollection('services'), getCollection('agents')]);
|
|
428
|
-
const routableResources = [...services, ...agents];
|
|
429
536
|
const message = await findEntry(params.messageCollection, params.messageId, params.messageVersion);
|
|
430
537
|
|
|
431
538
|
if (!message) {
|
|
@@ -433,21 +540,13 @@ export async function getProducersOfMessage(params: { messageId: string; message
|
|
|
433
540
|
error: `Message not found: ${params.messageId} (${describeVersion(params.messageVersion)}) in ${params.messageCollection}`,
|
|
434
541
|
};
|
|
435
542
|
}
|
|
436
|
-
const messageVersion = message.data.version;
|
|
437
543
|
|
|
438
|
-
const producers =
|
|
439
|
-
const sends = (resource.data as any).sends || [];
|
|
440
|
-
return sends.some((send: any) => {
|
|
441
|
-
const idMatch = send.id === params.messageId;
|
|
442
|
-
if (!send.version || send.version === 'latest') return idMatch;
|
|
443
|
-
return idMatch && send.version === messageVersion;
|
|
444
|
-
});
|
|
445
|
-
});
|
|
544
|
+
const producers = await getResourcesThat('sends', message);
|
|
446
545
|
|
|
447
546
|
return {
|
|
448
547
|
message: {
|
|
449
548
|
id: params.messageId,
|
|
450
|
-
version:
|
|
549
|
+
version: message.data.version,
|
|
451
550
|
collection: params.messageCollection,
|
|
452
551
|
},
|
|
453
552
|
producers: producers.map((s) => ({
|
|
@@ -504,8 +603,6 @@ export async function getC4Diagram(params: { viewId?: string }) {
|
|
|
504
603
|
* Get resources that consume (receive) a specific message
|
|
505
604
|
*/
|
|
506
605
|
export async function getConsumersOfMessage(params: { messageId: string; messageVersion?: string; messageCollection: string }) {
|
|
507
|
-
const [services, agents] = await Promise.all([getCollection('services'), getCollection('agents')]);
|
|
508
|
-
const routableResources = [...services, ...agents];
|
|
509
606
|
const message = await findEntry(params.messageCollection, params.messageId, params.messageVersion);
|
|
510
607
|
|
|
511
608
|
if (!message) {
|
|
@@ -513,21 +610,13 @@ export async function getConsumersOfMessage(params: { messageId: string; message
|
|
|
513
610
|
error: `Message not found: ${params.messageId} (${describeVersion(params.messageVersion)}) in ${params.messageCollection}`,
|
|
514
611
|
};
|
|
515
612
|
}
|
|
516
|
-
const messageVersion = message.data.version;
|
|
517
613
|
|
|
518
|
-
const consumers =
|
|
519
|
-
const receives = (resource.data as any).receives || [];
|
|
520
|
-
return receives.some((receive: any) => {
|
|
521
|
-
const idMatch = receive.id === params.messageId;
|
|
522
|
-
if (!receive.version || receive.version === 'latest') return idMatch;
|
|
523
|
-
return idMatch && receive.version === messageVersion;
|
|
524
|
-
});
|
|
525
|
-
});
|
|
614
|
+
const consumers = await getResourcesThat('receives', message);
|
|
526
615
|
|
|
527
616
|
return {
|
|
528
617
|
message: {
|
|
529
618
|
id: params.messageId,
|
|
530
|
-
version:
|
|
619
|
+
version: message.data.version,
|
|
531
620
|
collection: params.messageCollection,
|
|
532
621
|
},
|
|
533
622
|
consumers: consumers.map((s) => ({
|
|
@@ -545,8 +634,6 @@ export async function getConsumersOfMessage(params: { messageId: string; message
|
|
|
545
634
|
* Returns all affected services (producers and consumers) and their owners
|
|
546
635
|
*/
|
|
547
636
|
export async function analyzeChangeImpact(params: { messageId: string; messageVersion?: string; messageCollection: string }) {
|
|
548
|
-
const [services, agents] = await Promise.all([getCollection('services'), getCollection('agents')]);
|
|
549
|
-
const routableResources = [...services, ...agents];
|
|
550
637
|
const message = await findEntry(params.messageCollection, params.messageId, params.messageVersion);
|
|
551
638
|
|
|
552
639
|
if (!message) {
|
|
@@ -556,17 +643,7 @@ export async function analyzeChangeImpact(params: { messageId: string; messageVe
|
|
|
556
643
|
}
|
|
557
644
|
const messageVersion = message.data.version;
|
|
558
645
|
|
|
559
|
-
|
|
560
|
-
const producers = routableResources.filter((resource) => {
|
|
561
|
-
const sends = (resource.data as any).sends || [];
|
|
562
|
-
return sends.some((send: any) => send.id === params.messageId);
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
// Find consumers
|
|
566
|
-
const consumers = routableResources.filter((resource) => {
|
|
567
|
-
const receives = (resource.data as any).receives || [];
|
|
568
|
-
return receives.some((receive: any) => receive.id === params.messageId);
|
|
569
|
-
});
|
|
646
|
+
const [producers, consumers] = await Promise.all([getResourcesThat('sends', message), getResourcesThat('receives', message)]);
|
|
570
647
|
|
|
571
648
|
// Collect all affected teams/owners
|
|
572
649
|
const affectedOwners = new Set<string>();
|
|
@@ -789,19 +866,10 @@ export async function findMessageBySchemaId(params: {
|
|
|
789
866
|
const matches = getItemsFromCollectionByIdAndSemverOrLatest(collection, params.messageId, params.messageVersion);
|
|
790
867
|
const resource = matches[0];
|
|
791
868
|
if (resource) {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const producers = routableResources.filter((resource) => {
|
|
797
|
-
const sends = (resource.data as any).sends || [];
|
|
798
|
-
return sends.some((send: any) => send.id === params.messageId);
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
const consumers = routableResources.filter((resource) => {
|
|
802
|
-
const receives = (resource.data as any).receives || [];
|
|
803
|
-
return receives.some((receive: any) => receive.id === params.messageId);
|
|
804
|
-
});
|
|
869
|
+
const [producers, consumers] = await Promise.all([
|
|
870
|
+
getResourcesThat('sends', resource),
|
|
871
|
+
getResourcesThat('receives', resource),
|
|
872
|
+
]);
|
|
805
873
|
|
|
806
874
|
return {
|
|
807
875
|
resource: {
|
|
@@ -1258,23 +1326,23 @@ export async function getCustomDoc(params: { id: string; section?: string }) {
|
|
|
1258
1326
|
|
|
1259
1327
|
export const toolDescriptions = {
|
|
1260
1328
|
getResources:
|
|
1261
|
-
'Use this tool to
|
|
1329
|
+
'Use this tool to list the resources in a collection of EventCatalog (events, commands, queries, services, agents, domains, systems, flows, channels, entities, containers, data products, diagrams or ADRs). Supports pagination via cursor and filtering by search term (searches name, id, and summary).',
|
|
1262
1330
|
getResource:
|
|
1263
1331
|
'Use this tool to get a specific resource from EventCatalog by its id. Returns the frontmatter and markdown documentation. Omit the version to get the latest version.',
|
|
1264
1332
|
getMessagesProducedOrConsumedByResource:
|
|
1265
|
-
'Use this tool to get the messages (events, commands, queries) a service, agent or
|
|
1333
|
+
'Use this tool to get the messages (events, commands, queries) a service, agent, domain or system sends and receives. Each message is resolved to its catalog id, version, name and summary; messages that are referenced but not documented in the catalog are marked notFound. For a domain or system, this includes what the services, agents, systems and subdomains inside it send and receive, and each message lists `by`: the resources that send or receive it (e.g. services/order-service). Omit the version to use the latest version of the resource.',
|
|
1266
1334
|
getSchemaForResource:
|
|
1267
1335
|
'Use this tool to get the schema or specifications (openapi or asyncapi or graphql) for a resource by its id and version',
|
|
1268
1336
|
findResourcesByOwner:
|
|
1269
1337
|
'Use this tool to find all resources (agents, services, events, commands, etc.) owned by a specific team or user',
|
|
1270
1338
|
getProducersOfMessage:
|
|
1271
|
-
'Use this tool to find which agents or
|
|
1339
|
+
'Use this tool to find which services, agents or domains produce (send) a specific message (event, command, or query). Only the latest versions of those resources are included.',
|
|
1272
1340
|
getConsumersOfMessage:
|
|
1273
|
-
'Use this tool to find which agents or
|
|
1341
|
+
'Use this tool to find which services, agents or domains consume (receive) a specific message (event, command, or query). Only the latest versions of those resources are included.',
|
|
1274
1342
|
getC4Diagram:
|
|
1275
1343
|
'Use this tool to get the C4 diagram (or likeC4) in EventCatalog. These files usually have a .c4 extension on them',
|
|
1276
1344
|
analyzeChangeImpact:
|
|
1277
|
-
'Use this tool to analyze the impact of changing a message. Returns all affected agents and
|
|
1345
|
+
'Use this tool to analyze the impact of changing a message. Returns all affected services, agents and domains (producers and consumers), the teams that own them, and the blast radius of the change',
|
|
1278
1346
|
explainBusinessFlow:
|
|
1279
1347
|
'Use this tool to get detailed information about a business flow (state machine). Returns the flow definition, steps, mermaid diagram if available, and related agents and services',
|
|
1280
1348
|
getTeams:
|
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Domains and systems have their own routes next to their Diagram pages
|
|
6
6
|
* (visualiser/domains/[id]/[version]), which would otherwise handle these requests first.
|
|
7
|
+
* Their levels are served at /visualiser/{domains|systems}/{id}/{version}/{overview|services}.mermaid.
|
|
7
8
|
*/
|
|
8
9
|
import { isAuthEnabled, isVisualiserEnabled } from '@utils/feature';
|
|
9
10
|
import { pageDataLoader } from '@utils/page-loaders/page-data-loader';
|
|
10
11
|
import { getArchitectureDiagramGraph } from '@utils/node-graphs/architecture-diagram';
|
|
11
|
-
import {
|
|
12
|
-
|
|
12
|
+
import {
|
|
13
|
+
ARCHITECTURE_DIAGRAM_DETAILS,
|
|
14
|
+
isArchitectureDiagramCollection,
|
|
15
|
+
type ArchitectureDiagramDetail,
|
|
16
|
+
} from '@utils/node-graphs/architecture-diagram-types';
|
|
17
|
+
import { convertToMermaid } from '@eventcatalog/visualiser/layout';
|
|
13
18
|
import type { PageTypes } from '@types';
|
|
14
19
|
|
|
15
20
|
export type MermaidDiagramType =
|
|
@@ -34,7 +39,35 @@ export async function getMermaidDiagramPaths(types: MermaidDiagramType[]) {
|
|
|
34
39
|
);
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
/** Levels served at /visualiser/{domains|systems}/{id}/{version}/{level}.mermaid ({version}.mermaid is the full diagram) */
|
|
43
|
+
export const MERMAID_LEVELS = ['overview', 'services'] as const satisfies ArchitectureDiagramDetail[];
|
|
44
|
+
|
|
45
|
+
const LEVEL_DESCRIPTIONS: Record<ArchitectureDiagramDetail, string> = {
|
|
46
|
+
overview: 'L1 overview (domains, systems and their relationships)',
|
|
47
|
+
services: 'L2 services and data stores',
|
|
48
|
+
full: 'L3 full (services, messages and channels)',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Every version of each domain or system with each level, for building their Mermaid files in static builds */
|
|
52
|
+
export async function getMermaidLevelPaths(type: 'domains' | 'systems') {
|
|
53
|
+
const paths = await getMermaidDiagramPaths([type]);
|
|
54
|
+
return paths.flatMap(({ id, version }) => MERMAID_LEVELS.map((level) => ({ params: { id, version, level } })));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isMermaidDetail = (detail: string): detail is ArchitectureDiagramDetail =>
|
|
58
|
+
(ARCHITECTURE_DIAGRAM_DETAILS as readonly string[]).includes(detail);
|
|
59
|
+
|
|
60
|
+
export async function createMermaidDiagramResponse({
|
|
61
|
+
type,
|
|
62
|
+
id,
|
|
63
|
+
version,
|
|
64
|
+
detail = 'full',
|
|
65
|
+
}: {
|
|
66
|
+
type?: string;
|
|
67
|
+
id?: string;
|
|
68
|
+
version?: string;
|
|
69
|
+
detail?: string;
|
|
70
|
+
}) {
|
|
38
71
|
if (!type || !id || !version || !isVisualiserEnabled()) {
|
|
39
72
|
return new Response('Not found', { status: 404 });
|
|
40
73
|
}
|
|
@@ -44,9 +77,18 @@ export async function createMermaidDiagramResponse({ type, id, version }: { type
|
|
|
44
77
|
return new Response(`Unsupported type: ${type}`, { status: 400 });
|
|
45
78
|
}
|
|
46
79
|
|
|
80
|
+
if (!isMermaidDetail(detail)) {
|
|
81
|
+
return new Response('Not found', { status: 404 });
|
|
82
|
+
}
|
|
83
|
+
|
|
47
84
|
try {
|
|
48
|
-
// Built the same way as the resource's Diagram page
|
|
49
|
-
const { nodes, edges } = await getArchitectureDiagramGraph({
|
|
85
|
+
// Built the same way as the resource's Diagram page, at the level asked for
|
|
86
|
+
const { nodes, edges } = await getArchitectureDiagramGraph({
|
|
87
|
+
collection: type,
|
|
88
|
+
id,
|
|
89
|
+
version,
|
|
90
|
+
...(detail !== 'full' && { detail }),
|
|
91
|
+
});
|
|
50
92
|
|
|
51
93
|
if (!nodes || nodes.length === 0) {
|
|
52
94
|
return new Response('No diagram data available', { status: 404 });
|
|
@@ -60,6 +102,7 @@ export async function createMermaidDiagramResponse({ type, id, version }: { type
|
|
|
60
102
|
// Add header comment with metadata
|
|
61
103
|
const header = `%% EventCatalog Mermaid Diagram
|
|
62
104
|
%% Resource: ${type}/${id} (v${version})
|
|
105
|
+
%% Level: ${LEVEL_DESCRIPTIONS[detail]}
|
|
63
106
|
%% Generated: ${new Date().toISOString()}
|
|
64
107
|
|
|
65
108
|
`;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API endpoint to return mermaid diagram code for a level of a domain diagram
|
|
3
|
+
* URL: /visualiser/domains/{id}/{version}/{overview|services}.mermaid
|
|
4
|
+
*
|
|
5
|
+
* `overview` is level 1 (domains, systems and their relationships) and `services` level 2
|
|
6
|
+
* (services and data stores), as on the Diagram page. {version}.mermaid is the full diagram (level 3).
|
|
7
|
+
*/
|
|
8
|
+
import type { APIRoute, GetStaticPaths } from 'astro';
|
|
9
|
+
import { isAuthEnabled } from '@utils/feature';
|
|
10
|
+
import { createMermaidDiagramResponse, getMermaidLevelPaths } from '../../../_mermaid-route';
|
|
11
|
+
|
|
12
|
+
// Prerender static pages when auth is disabled, use SSR when auth is enabled
|
|
13
|
+
export const prerender = !isAuthEnabled();
|
|
14
|
+
|
|
15
|
+
export const getStaticPaths: GetStaticPaths = () => getMermaidLevelPaths('domains');
|
|
16
|
+
|
|
17
|
+
export const GET: APIRoute = async ({ params }) =>
|
|
18
|
+
createMermaidDiagramResponse({ type: 'domains', id: params.id, version: params.version, detail: params.level });
|