@eventcatalog/core 5.0.0-beta.1 → 5.0.0-beta.11
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/LICENSE +227 -128
- package/README.md +4 -4
- package/dist/analytics/analytics.cjs +18 -3
- package/dist/analytics/analytics.d.cts +1 -1
- package/dist/analytics/analytics.d.ts +1 -1
- package/dist/analytics/analytics.js +3 -2
- package/dist/analytics/log-build.cjs +71 -35
- package/dist/analytics/log-build.js +5 -4
- package/dist/analytics/telemetry.cjs +42 -0
- package/dist/analytics/telemetry.d.cts +20 -0
- package/dist/analytics/telemetry.d.ts +20 -0
- package/dist/analytics/telemetry.js +8 -0
- package/dist/{chunk-KYJZJLBD.js → chunk-4YFM63DS.js} +9 -3
- package/dist/{chunk-UDS3SYLZ.js → chunk-76DAAJLV.js} +56 -33
- package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
- package/dist/chunk-CPW6G62V.js +17 -0
- package/dist/{chunk-K2ACDO7Y.js → chunk-LQZ7IG7B.js} +1 -1
- package/dist/{chunk-S2QGT7ZF.js → chunk-MFEMKQZ6.js} +1 -1
- package/dist/{chunk-Q6SD5SNQ.js → chunk-WM4QYMIA.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +204 -124
- package/dist/eventcatalog.config.d.cts +59 -1
- package/dist/eventcatalog.config.d.ts +59 -1
- package/dist/eventcatalog.js +30 -11
- package/dist/federation/federate.js +2 -2
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
- package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
- package/dist/runtime-dependencies/manifest.json +8 -1
- package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
- package/dist/runtime-dependencies/yjs.mjs +3 -0
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/dist/utils/license-status.cjs +71 -45
- package/dist/utils/license-status.d.cts +14 -5
- package/dist/utils/license-status.d.ts +14 -5
- package/dist/utils/license-status.js +1 -1
- package/eventcatalog/astro.config.mjs +5 -0
- package/eventcatalog/integrations/storage.mjs +20 -0
- package/eventcatalog/integrations/studio-server.mjs +32 -0
- package/eventcatalog/src/components/Grids/utils.tsx +0 -23
- package/eventcatalog/src/components/Header.astro +3 -3
- package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +16 -7
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +10 -0
- package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
- package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
- package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
- package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
- package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
- package/eventcatalog/src/features/ai/chat-api.ts +3 -3
- package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
- package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +22 -0
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +276 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +210 -0
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +75 -74
- package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
- package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
- package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
- package/eventcatalog/src/features/server/start.ts +25 -0
- package/eventcatalog/src/features/storage/database.ts +88 -0
- package/eventcatalog/src/features/storage/migrations.ts +85 -0
- package/eventcatalog/src/features/storage/sql/database.sql +5 -0
- package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
- package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
- package/eventcatalog/src/features/storage/sql.ts +28 -0
- package/eventcatalog/src/features/studio/agent-choreography.ts +381 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +170 -0
- package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
- package/eventcatalog/src/features/studio/api/client.ts +35 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +414 -0
- package/eventcatalog/src/features/studio/canvas-doc.ts +414 -0
- package/eventcatalog/src/features/studio/canvas-preview.ts +53 -0
- package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
- package/eventcatalog/src/features/studio/catalog.ts +171 -0
- package/eventcatalog/src/features/studio/clipboard.ts +97 -0
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +154 -0
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +370 -0
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +117 -0
- package/eventcatalog/src/features/studio/components/Comments.tsx +770 -0
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +281 -0
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +144 -0
- package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +145 -0
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +81 -0
- package/eventcatalog/src/features/studio/components/DragGhost.tsx +115 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +34 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +314 -0
- package/eventcatalog/src/features/studio/components/Picture.tsx +21 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +286 -0
- package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +212 -0
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +260 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1216 -0
- package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
- package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +255 -0
- package/eventcatalog/src/features/studio/components/icons.ts +53 -0
- package/eventcatalog/src/features/studio/components/status.ts +53 -0
- package/eventcatalog/src/features/studio/edges.ts +56 -0
- package/eventcatalog/src/features/studio/flow-state.ts +92 -0
- package/eventcatalog/src/features/studio/grouping.ts +226 -0
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +46 -0
- package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +347 -0
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +104 -0
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +589 -0
- package/eventcatalog/src/features/studio/identity.ts +37 -0
- package/eventcatalog/src/features/studio/layout.ts +42 -0
- package/eventcatalog/src/features/studio/levels.ts +282 -0
- package/eventcatalog/src/features/studio/node-types.ts +205 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +68 -0
- package/eventcatalog/src/features/studio/pages/index.astro +784 -0
- package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
- package/eventcatalog/src/features/studio/placement.ts +197 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +510 -0
- package/eventcatalog/src/features/studio/server/runtime.ts +491 -0
- package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
- package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
- package/eventcatalog/src/features/studio/server/storage.ts +90 -0
- package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
- package/eventcatalog/src/features/studio/studio.css +273 -0
- package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -5
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
- package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
- package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
- package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
- package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
- package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
- package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
- package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
- package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
- package/eventcatalog/src/styles/tailwind.css +2 -2
- package/eventcatalog/src/utils/collections/channels.ts +56 -1
- package/eventcatalog/src/utils/collections/flows.ts +50 -1
- package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
- package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
- package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
- package/eventcatalog/src/utils/collections/resources.ts +310 -0
- package/eventcatalog/src/utils/collections/util.ts +31 -0
- package/eventcatalog/src/utils/feature.ts +8 -0
- package/eventcatalog/src/utils/git.ts +11 -5
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
- package/eventcatalog/src/utils/trial.ts +1 -1
- package/package.json +17 -12
- package/dist/runtime-dependencies/shelljs.mjs +0 -3
- package/eventcatalog/public/studio-bg.png +0 -0
- package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
- package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
- package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
- package/eventcatalog/src/components/Grids/components.tsx +0 -170
- package/eventcatalog/src/components/Grids/message-link.ts +0 -38
- package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
- package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
- package/eventcatalog/src/pages/studio.astro +0 -316
- package/dist/{chunk-AYNZXUB5.js → chunk-KHACCTW2.js} +3 -3
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import type { Edge, Node, XYPosition } from '@xyflow/react';
|
|
2
|
+
import type * as Y from 'yjs';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { buildNode, getCanvasMaps, getCanvasStatus, readCanvas, updateNodeData } from './canvas-doc';
|
|
5
|
+
import type { CatalogRelation, CatalogResource } from './catalog-resources';
|
|
6
|
+
import {
|
|
7
|
+
buildContainer,
|
|
8
|
+
canBeContainer,
|
|
9
|
+
catalogNodeData,
|
|
10
|
+
containerTypeFor,
|
|
11
|
+
getCatalogLink,
|
|
12
|
+
getRelatedEdges,
|
|
13
|
+
indexRelations,
|
|
14
|
+
} from './catalog';
|
|
15
|
+
import { findGroupAtPoint, getAbsolutePosition, sizeOf } from './grouping';
|
|
16
|
+
import { placeNodes, type Link } from './placement';
|
|
17
|
+
import {
|
|
18
|
+
canGoInContainer,
|
|
19
|
+
COMPONENT_TYPES,
|
|
20
|
+
getNodeDefinition,
|
|
21
|
+
getNodeLabel,
|
|
22
|
+
getNodeName,
|
|
23
|
+
getNoteLevel,
|
|
24
|
+
getResource,
|
|
25
|
+
isGroupType,
|
|
26
|
+
isOnLevel,
|
|
27
|
+
isVersioned,
|
|
28
|
+
isWrittenOnCanvas,
|
|
29
|
+
updateResource,
|
|
30
|
+
} from './node-types';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* What agents can do on a canvas, shared by the MCP tools (on the server) and the WebMCP tools (in the
|
|
34
|
+
* browser), so both describe and change a canvas the same way.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export const CATALOG_COLLECTIONS = [
|
|
38
|
+
'domains',
|
|
39
|
+
'systems',
|
|
40
|
+
'services',
|
|
41
|
+
'events',
|
|
42
|
+
'commands',
|
|
43
|
+
'queries',
|
|
44
|
+
'channels',
|
|
45
|
+
'containers',
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
// ---- Inputs ----
|
|
49
|
+
|
|
50
|
+
export const nodeSpecsSchema = z
|
|
51
|
+
.array(
|
|
52
|
+
z.object({
|
|
53
|
+
ref: z.string().optional().describe('Your name for this node, to use in edges'),
|
|
54
|
+
resource: z
|
|
55
|
+
.object({ collection: z.enum(CATALOG_COLLECTIONS), id: z.string() })
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('A catalog resource to add (latest version)'),
|
|
58
|
+
type: z.enum(COMPONENT_TYPES).optional().describe('For a new component: what it is'),
|
|
59
|
+
name: z.string().optional().describe('For a new component: its name'),
|
|
60
|
+
summary: z.string().optional().describe('For a new component: what it does. For a note or text: its text'),
|
|
61
|
+
version: z.string().optional().describe('For a new component: its version, e.g. 1.0.0 (defaults to 0.0.1)'),
|
|
62
|
+
container: z
|
|
63
|
+
.boolean()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe(
|
|
66
|
+
'For a domain or system from the catalog: add it as a container that other nodes sit inside, rather than a card'
|
|
67
|
+
),
|
|
68
|
+
inside: z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe('A ref (or node id) of a container (a domain or system added as one) to put this node inside'),
|
|
72
|
+
x: z.number().optional(),
|
|
73
|
+
y: z.number().optional(),
|
|
74
|
+
})
|
|
75
|
+
)
|
|
76
|
+
.min(1);
|
|
77
|
+
|
|
78
|
+
export const edgeSpecsSchema = z.array(
|
|
79
|
+
z.object({
|
|
80
|
+
from: z.string().describe('A ref from nodes, or the id of a node already on the canvas'),
|
|
81
|
+
to: z.string().describe('A ref from nodes, or the id of a node already on the canvas'),
|
|
82
|
+
label: z.string().optional().describe('Defaults to EventCatalog wording, e.g. "publishes event"'),
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
export const ADD_TO_CANVAS_DESCRIPTION = [
|
|
87
|
+
'Adds nodes (and connections between them) to the canvas, live for everyone on it.',
|
|
88
|
+
'A node is either a catalog resource (resource: { collection, id }), shown with its real name and summary and automatically connected to the related resources already on the canvas (e.g. a service to the events it publishes), or a new component (type, name, summary) for things that are not in the catalog yet, like a proposed service.',
|
|
89
|
+
'Give each node a ref to connect new nodes to each other in edges.',
|
|
90
|
+
'Domains and systems can be containers (container: true for catalog ones, or type domain-group / system-group for new ones): put services, messages and data stores inside one with inside (its ref or node id), e.g. a domain container with its system containers inside, and their services inside those.',
|
|
91
|
+
'Leave out x/y and each node is placed like an architect would: next to what it connects to (in edges, or as a related catalog resource), following the flow left to right (producers on the left of their messages, consumers on the right), lined up with it, and stacked when several connect to the same node. Nothing already on the canvas moves, and nodes connected to nothing go below what is there.',
|
|
92
|
+
"Give x/y (a node's centre, like the positions from reading the canvas) only to put a node somewhere specific. Nodes are about 240 wide and 110 tall: leave about 200 between connected nodes for the labels on their connections.",
|
|
93
|
+
].join(' ');
|
|
94
|
+
|
|
95
|
+
// ---- Catalog ----
|
|
96
|
+
|
|
97
|
+
export type CatalogIndex = { resourcesByKey: Map<string, CatalogResource>; relationsByKey: Map<string, CatalogRelation[]> };
|
|
98
|
+
|
|
99
|
+
export const indexCatalog = (resources: CatalogResource[], relations: CatalogRelation[]): CatalogIndex => ({
|
|
100
|
+
resourcesByKey: new Map(resources.map((resource) => [resource.key, resource])),
|
|
101
|
+
relationsByKey: indexRelations(relations),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ---- Reading ----
|
|
105
|
+
|
|
106
|
+
type NodeLookup = Map<string, Node>;
|
|
107
|
+
const lookupOf = (node: Node, lookup?: NodeLookup) => lookup ?? new Map([[node.id, node]]);
|
|
108
|
+
|
|
109
|
+
/** Something on the canvas, as an agent sees it */
|
|
110
|
+
export const describeNode = (node: Node, lookup?: NodeLookup) => {
|
|
111
|
+
const resource = getResource(node.type, node.data);
|
|
112
|
+
const link = getCatalogLink(node);
|
|
113
|
+
const version = link?.version ?? (typeof resource.version === 'string' ? resource.version : undefined);
|
|
114
|
+
return {
|
|
115
|
+
id: node.id,
|
|
116
|
+
type: getNodeLabel(node.type),
|
|
117
|
+
...(!isWrittenOnCanvas(node.type) && { name: resource.name }),
|
|
118
|
+
...(resource.summary ? { summary: resource.summary } : {}),
|
|
119
|
+
...(isWrittenOnCanvas(node.type) ? { text: node.data.text } : {}),
|
|
120
|
+
// Notes added on L1 or L2 are only shown there (people see the rest at L3)
|
|
121
|
+
...(getNoteLevel(node) !== undefined && getNoteLevel(node) !== 3 && { onLevel: `L${getNoteLevel(node)}` }),
|
|
122
|
+
...(link && { catalogResource: link.key.replace(':', '/') }),
|
|
123
|
+
...(version && { version }),
|
|
124
|
+
/** The node's centre on the canvas, like the x/y agents give */
|
|
125
|
+
position: (({ x, y }) => ({ x: Math.round(x), y: Math.round(y) }))(nodeCenter(node, lookup)),
|
|
126
|
+
size: sizeOf(node),
|
|
127
|
+
...(isGroupType(node.type) && { container: true }),
|
|
128
|
+
...(node.parentId && { inside: node.parentId }),
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** A node's centre on the canvas (nodes in containers are positioned relative to them) */
|
|
133
|
+
export const nodeCenter = (node: Node, lookup?: NodeLookup) => {
|
|
134
|
+
const { width, height } = sizeOf(node);
|
|
135
|
+
const position = getAbsolutePosition(node, lookupOf(node, lookup));
|
|
136
|
+
return { x: position.x + width / 2, y: position.y + height / 2 };
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** Where a node's position goes for it to be centred on a canvas point (agents give centres) */
|
|
140
|
+
export const topLeftFor = (node: Node, center: XYPosition, lookup?: NodeLookup) => {
|
|
141
|
+
const { width, height } = sizeOf(node);
|
|
142
|
+
const parent = node.parentId ? lookup?.get(node.parentId) : undefined;
|
|
143
|
+
const parentPosition = parent ? getAbsolutePosition(parent, lookupOf(parent, lookup)) : { x: 0, y: 0 };
|
|
144
|
+
return { x: center.x - width / 2 - parentPosition.x, y: center.y - height / 2 - parentPosition.y };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** Everything on a canvas, as an agent sees it */
|
|
148
|
+
export const describeCanvas = (doc: Y.Doc) => {
|
|
149
|
+
const canvas = readCanvas(doc);
|
|
150
|
+
const lookup = new Map(canvas.nodes.map((node) => [node.id, node]));
|
|
151
|
+
const names = new Map(canvas.nodes.map((node) => [node.id, getNodeName(node.type, node.data, node.id)]));
|
|
152
|
+
const lastChange = canvas.meta.statusHistory?.at(-1);
|
|
153
|
+
return {
|
|
154
|
+
title: canvas.meta.title,
|
|
155
|
+
status: getCanvasStatus(canvas.meta),
|
|
156
|
+
...(lastChange && {
|
|
157
|
+
statusChanged: {
|
|
158
|
+
by: lastChange.by.name,
|
|
159
|
+
at: new Date(lastChange.at).toISOString(),
|
|
160
|
+
...(lastChange.note && { note: lastChange.note }),
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
nodes: canvas.nodes.map((node) => describeNode(node, lookup)),
|
|
164
|
+
connections: canvas.edges.map((edge) => ({
|
|
165
|
+
id: edge.id,
|
|
166
|
+
from: edge.source,
|
|
167
|
+
to: edge.target,
|
|
168
|
+
description: `${names.get(edge.source)} ${String(edge.label || '→').replace(/\s+/g, ' ')} ${names.get(edge.target)}`,
|
|
169
|
+
})),
|
|
170
|
+
comments: canvas.threads.map((thread) => ({
|
|
171
|
+
threadId: thread.id,
|
|
172
|
+
...(thread.nodeId ? { onNode: thread.nodeId } : { position: thread.position }),
|
|
173
|
+
resolved: thread.resolved,
|
|
174
|
+
messages: thread.messages.map((message) => ({ from: message.author.name, text: message.text })),
|
|
175
|
+
})),
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// ---- Changing ----
|
|
180
|
+
|
|
181
|
+
export type NodeSpecs = z.output<typeof nodeSpecsSchema>;
|
|
182
|
+
export type EdgeSpecs = z.output<typeof edgeSpecsSchema>;
|
|
183
|
+
/**
|
|
184
|
+
* A node to add: built and placed, with the catalog resource it stands for (to connect it to related ones).
|
|
185
|
+
* Its node's position is relative to its container, as the canvas is now; `canvasPosition` is where it goes on
|
|
186
|
+
* the canvas, which stays the same when containers grow to fit what's added (and move what's in them).
|
|
187
|
+
*/
|
|
188
|
+
export type PlannedNode = { ref?: string; node: Node; name: string; resource?: CatalogResource; canvasPosition: XYPosition };
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Builds the nodes to add (catalog resources, as cards or containers, or new components), and places them:
|
|
192
|
+
* where asked, or next to what they connect to (the edges asked for, and related catalog resources)
|
|
193
|
+
*/
|
|
194
|
+
export const planNodes = (existing: Node[], specs: NodeSpecs, catalog: CatalogIndex, edges: EdgeSpecs = []) => {
|
|
195
|
+
const lookup: NodeLookup = new Map(existing.map((node) => [node.id, node]));
|
|
196
|
+
const refs = new Map<string, Node>();
|
|
197
|
+
const planned: PlannedNode[] = [];
|
|
198
|
+
const toPlace: string[] = [];
|
|
199
|
+
const errors: string[] = [];
|
|
200
|
+
const hasPosition = (spec: { x?: number; y?: number }) => spec.x !== undefined && spec.y !== undefined;
|
|
201
|
+
const centerOf = (spec: { x?: number; y?: number }) => (hasPosition(spec) ? { x: spec.x!, y: spec.y! } : { x: 0, y: 0 });
|
|
202
|
+
|
|
203
|
+
// Inside a container, at the canvas point asked for (relative to the container)
|
|
204
|
+
const putInside = (node: Node, parent: Node, spec: { x?: number; y?: number }): Node => {
|
|
205
|
+
if (!hasPosition(spec)) return { ...node, parentId: parent.id };
|
|
206
|
+
const { width, height } = sizeOf(node);
|
|
207
|
+
const parentPosition = getAbsolutePosition(parent, lookup);
|
|
208
|
+
return {
|
|
209
|
+
...node,
|
|
210
|
+
parentId: parent.id,
|
|
211
|
+
position: { x: spec.x! - width / 2 - parentPosition.x, y: spec.y! - height / 2 - parentPosition.y },
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
for (const spec of specs) {
|
|
216
|
+
let built: { node: Node; name: string; resource?: CatalogResource } | undefined;
|
|
217
|
+
if (spec.resource) {
|
|
218
|
+
const resource = catalog.resourcesByKey.get(`${spec.resource.collection}:${spec.resource.id}`);
|
|
219
|
+
if (!resource) {
|
|
220
|
+
errors.push(`No ${spec.resource.collection} resource "${spec.resource.id}" in the catalog`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const asContainer = spec.container && canBeContainer(resource);
|
|
224
|
+
const node = asContainer
|
|
225
|
+
? buildContainer(resource, centerOf(spec))
|
|
226
|
+
: buildNode(resource.node.type, catalogNodeData(resource), centerOf(spec));
|
|
227
|
+
built = { node, name: resource.name, resource };
|
|
228
|
+
} else {
|
|
229
|
+
const definition = getNodeDefinition(spec.type);
|
|
230
|
+
if (!definition) {
|
|
231
|
+
errors.push(`Give each node a resource, or a type (one of ${COMPONENT_TYPES.join(', ')})`);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const data = isWrittenOnCanvas(definition.type)
|
|
235
|
+
? { ...definition.createData(), text: spec.summary ?? spec.name ?? '' }
|
|
236
|
+
: updateResource(definition.type, definition.createData(), {
|
|
237
|
+
...(spec.name && { name: spec.name }),
|
|
238
|
+
...(spec.summary && { summary: spec.summary }),
|
|
239
|
+
...(spec.version && isVersioned(definition.type) && { version: spec.version }),
|
|
240
|
+
});
|
|
241
|
+
const node = buildNode(definition.type, data, centerOf(spec));
|
|
242
|
+
built = { node, name: String(getResource(node.type, node.data).name ?? definition.label) };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (spec.inside && !canGoInContainer(built.node.type)) {
|
|
246
|
+
errors.push(`Notes aren't put in containers: "${spec.ref ?? built.name}" was put on the canvas instead`);
|
|
247
|
+
} else if (spec.inside) {
|
|
248
|
+
const parent = refs.get(spec.inside) ?? lookup.get(spec.inside);
|
|
249
|
+
if (parent && isGroupType(parent.type)) built.node = putInside(built.node, parent, spec);
|
|
250
|
+
else errors.push(`"${spec.inside}" is not a container on the canvas (a domain or system added as a container)`);
|
|
251
|
+
}
|
|
252
|
+
if (!hasPosition(spec)) toPlace.push(built.node.id);
|
|
253
|
+
lookup.set(built.node.id, built.node);
|
|
254
|
+
if (spec.ref) refs.set(spec.ref, built.node);
|
|
255
|
+
planned.push({ ref: spec.ref, ...built, canvasPosition: getAbsolutePosition(built.node, lookup) });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// What the new nodes connect to: the edges asked for, and related catalog resources (as they'll be connected)
|
|
259
|
+
const links = new Map<string, Link>();
|
|
260
|
+
const link = (source: string, target: string) => links.set(`${source}>${target}`, { source, target });
|
|
261
|
+
const nodeIdOf = (refOrId: string) => refs.get(refOrId)?.id ?? refOrId;
|
|
262
|
+
edges.forEach((edge) => link(nodeIdOf(edge.from), nodeIdOf(edge.to)));
|
|
263
|
+
const nodesByKey = new Map<string, Node[]>();
|
|
264
|
+
for (const node of lookup.values()) {
|
|
265
|
+
const key = getCatalogLink(node)?.key;
|
|
266
|
+
if (key) nodesByKey.set(key, [...(nodesByKey.get(key) ?? []), node]);
|
|
267
|
+
}
|
|
268
|
+
for (const { node, resource } of planned) {
|
|
269
|
+
for (const relation of resource ? (catalog.relationsByKey.get(resource.key) ?? []) : []) {
|
|
270
|
+
const outgoing = relation.source === resource?.key;
|
|
271
|
+
for (const other of nodesByKey.get(outgoing ? relation.target : relation.source) ?? []) {
|
|
272
|
+
// Containers hold what they contain, rather than connect to it
|
|
273
|
+
if (relation.label === 'contains' && (isGroupType(node.type) || isGroupType(other.type))) continue;
|
|
274
|
+
if (outgoing) link(node.id, other.id);
|
|
275
|
+
else link(other.id, node.id);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Placed around what's on the canvas people edit (notes on L1 and L2 aren't there)
|
|
281
|
+
const onCanvas = [...lookup.values()].filter((node) => isOnLevel(node, 3));
|
|
282
|
+
const positions = placeNodes(onCanvas, toPlace, [...links.values()]);
|
|
283
|
+
const canvasPositions = new Map(planned.map((entry) => [entry.node.id, positions.get(entry.node.id) ?? entry.canvasPosition]));
|
|
284
|
+
for (const entry of planned) {
|
|
285
|
+
const canvasPosition = positions.get(entry.node.id);
|
|
286
|
+
if (!canvasPosition) continue;
|
|
287
|
+
const { parentId } = entry.node;
|
|
288
|
+
const parent = parentId ? lookup.get(parentId) : undefined;
|
|
289
|
+
const origin = parentId
|
|
290
|
+
? (canvasPositions.get(parentId) ?? (parent ? getAbsolutePosition(parent, lookup) : { x: 0, y: 0 }))
|
|
291
|
+
: { x: 0, y: 0 };
|
|
292
|
+
entry.canvasPosition = canvasPosition;
|
|
293
|
+
entry.node = { ...entry.node, position: { x: canvasPosition.x - origin.x, y: canvasPosition.y - origin.y } };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { planned, errors };
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/** Edges given by refs (or ids of nodes already on the canvas), as node ids */
|
|
300
|
+
export const resolveEdgeRefs = (edges: EdgeSpecs, planned: PlannedNode[]) => {
|
|
301
|
+
const refs = new Map(planned.flatMap((entry) => (entry.ref ? [[entry.ref, entry.node.id] as const] : [])));
|
|
302
|
+
return edges.map((edge) => ({ ...edge, from: refs.get(edge.from) ?? edge.from, to: refs.get(edge.to) ?? edge.to }));
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
export type NodeUpdate = { nodeId: string; name?: string; summary?: string; version?: string };
|
|
306
|
+
|
|
307
|
+
/** Renames, re-describes or re-versions a node. Catalog resources keep their catalog name, summary and version. */
|
|
308
|
+
export const updateCanvasNode = (doc: Y.Doc, { nodeId, name, summary, version }: NodeUpdate) =>
|
|
309
|
+
doc.transact(() => {
|
|
310
|
+
const node = getCanvasMaps(doc).nodes.get(nodeId);
|
|
311
|
+
if (!node) return { error: `No node "${nodeId}" on the canvas` };
|
|
312
|
+
if (!getCatalogLink(node)) {
|
|
313
|
+
updateNodeData(doc, nodeId, (data) =>
|
|
314
|
+
isWrittenOnCanvas(node.type)
|
|
315
|
+
? { ...data, text: summary ?? name ?? data.text }
|
|
316
|
+
: updateResource(node.type, data, {
|
|
317
|
+
...(name !== undefined && { name }),
|
|
318
|
+
...(summary !== undefined && { summary }),
|
|
319
|
+
...(version !== undefined && isVersioned(node.type) && { version }),
|
|
320
|
+
})
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return { updated: nodeId };
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ---- A catalog resource with its connections ----
|
|
327
|
+
|
|
328
|
+
const MESSAGE_COLLECTIONS = new Set(['events', 'commands', 'queries']);
|
|
329
|
+
|
|
330
|
+
/** What a group of connections is: what a service sends, receives and stores, or which services send or receive a message */
|
|
331
|
+
export type ConnectionGroupId = 'receives' | 'sends' | 'dataStores' | 'senders' | 'receivers';
|
|
332
|
+
export type ConnectionGroup = { id: ConnectionGroupId; label: string; resources: CatalogResource[] };
|
|
333
|
+
|
|
334
|
+
const GROUP_LABELS: Record<ConnectionGroupId, string> = {
|
|
335
|
+
receives: 'Messages it receives',
|
|
336
|
+
sends: 'Messages it sends',
|
|
337
|
+
dataStores: 'Data stores it reads or writes',
|
|
338
|
+
senders: 'Services that send it',
|
|
339
|
+
receivers: 'Services that receive it',
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/** Which group a related resource is in, for a service or a message (or none: not brought with it) */
|
|
343
|
+
const groupOf = (resource: CatalogResource, other: CatalogResource, outgoing: boolean): ConnectionGroupId | undefined => {
|
|
344
|
+
if (resource.collection === 'services') {
|
|
345
|
+
if (other.collection === 'containers') return 'dataStores';
|
|
346
|
+
if (MESSAGE_COLLECTIONS.has(other.collection)) return outgoing ? 'sends' : 'receives';
|
|
347
|
+
}
|
|
348
|
+
if (MESSAGE_COLLECTIONS.has(resource.collection) && other.collection === 'services') return outgoing ? 'receivers' : 'senders';
|
|
349
|
+
return undefined;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* What a catalog service or message connects to that isn't on the canvas yet, in groups (only the groups with
|
|
354
|
+
* something in them): a service's messages and data stores, or the services that send and receive a message
|
|
355
|
+
*/
|
|
356
|
+
export const getCatalogConnections = (
|
|
357
|
+
resource: CatalogResource,
|
|
358
|
+
catalog: CatalogIndex,
|
|
359
|
+
keysOnCanvas: ReadonlySet<string>
|
|
360
|
+
): ConnectionGroup[] => {
|
|
361
|
+
const groups = new Map<ConnectionGroupId, CatalogResource[]>();
|
|
362
|
+
const seen = new Set<string>();
|
|
363
|
+
for (const relation of catalog.relationsByKey.get(resource.key) ?? []) {
|
|
364
|
+
const outgoing = relation.source === resource.key;
|
|
365
|
+
const other = catalog.resourcesByKey.get(outgoing ? relation.target : relation.source);
|
|
366
|
+
if (!other || seen.has(other.key) || keysOnCanvas.has(other.key)) continue;
|
|
367
|
+
const group = groupOf(resource, other, outgoing);
|
|
368
|
+
if (!group) continue;
|
|
369
|
+
groups.set(group, [...(groups.get(group) ?? []), other]);
|
|
370
|
+
seen.add(other.key);
|
|
371
|
+
}
|
|
372
|
+
// In reading order: what comes in, then what goes out
|
|
373
|
+
return (['receives', 'senders', 'sends', 'receivers', 'dataStores'] as const).flatMap((id) => {
|
|
374
|
+
const resources = groups.get(id);
|
|
375
|
+
return resources ? [{ id, label: GROUP_LABELS[id], resources }] : [];
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* A catalog resource centred on a canvas point (inside the container there, if there is one), with the resources
|
|
381
|
+
* it connects to placed around it like an architect would (what comes in before it, what goes out after it), and
|
|
382
|
+
* the connections between them and to what's already on the canvas. A service's data stores go in its container
|
|
383
|
+
* with it. Positions are relative to the containers they're in, as the canvas is now.
|
|
384
|
+
*/
|
|
385
|
+
export const planWithConnections = (
|
|
386
|
+
existing: Node[],
|
|
387
|
+
resource: CatalogResource,
|
|
388
|
+
center: XYPosition,
|
|
389
|
+
include: CatalogResource[],
|
|
390
|
+
catalog: CatalogIndex
|
|
391
|
+
): { nodes: Node[]; edges: Edge[] } => {
|
|
392
|
+
const container = findGroupAtPoint(center, existing);
|
|
393
|
+
const specOf = (entry: CatalogResource) => ({
|
|
394
|
+
resource: { collection: entry.collection as (typeof CATALOG_COLLECTIONS)[number], id: entry.id },
|
|
395
|
+
});
|
|
396
|
+
const { planned } = planNodes(
|
|
397
|
+
existing,
|
|
398
|
+
[
|
|
399
|
+
{ ...specOf(resource), x: center.x, y: center.y, ...(container && { inside: container.id }) },
|
|
400
|
+
...include.map((entry) => ({
|
|
401
|
+
...specOf(entry),
|
|
402
|
+
...(container && entry.collection === 'containers' && { inside: container.id }),
|
|
403
|
+
})),
|
|
404
|
+
],
|
|
405
|
+
catalog
|
|
406
|
+
);
|
|
407
|
+
const placed = [...existing];
|
|
408
|
+
const edges: Edge[] = [];
|
|
409
|
+
for (const { node, resource: entry } of planned) {
|
|
410
|
+
if (entry) edges.push(...getRelatedEdges(entry, node.id, placed, catalog.relationsByKey, node.type));
|
|
411
|
+
placed.push(node);
|
|
412
|
+
}
|
|
413
|
+
return { nodes: planned.map((entry) => entry.node), edges };
|
|
414
|
+
};
|