@eventcatalog/core 5.0.0-beta.3 → 5.0.0-beta.30

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 (277) hide show
  1. package/LICENSE +234 -134
  2. package/README.md +5 -5
  3. package/dist/analytics/analytics.cjs +18 -3
  4. package/dist/analytics/analytics.d.cts +1 -1
  5. package/dist/analytics/analytics.d.ts +1 -1
  6. package/dist/analytics/analytics.js +3 -2
  7. package/dist/analytics/log-build.cjs +160 -77
  8. package/dist/analytics/log-build.d.cts +2 -2
  9. package/dist/analytics/log-build.d.ts +2 -2
  10. package/dist/analytics/log-build.js +7 -5
  11. package/dist/analytics/studio-usage.cjs +90 -0
  12. package/dist/analytics/studio-usage.d.cts +5 -0
  13. package/dist/analytics/studio-usage.d.ts +5 -0
  14. package/dist/analytics/studio-usage.js +10 -0
  15. package/dist/analytics/telemetry.cjs +42 -0
  16. package/dist/analytics/telemetry.d.cts +20 -0
  17. package/dist/analytics/telemetry.d.ts +20 -0
  18. package/dist/analytics/telemetry.js +8 -0
  19. package/dist/catalog-runtime.cjs +1 -1
  20. package/dist/{chunk-RFIXKATC.js → chunk-4G7FSRPN.js} +35 -42
  21. package/dist/{chunk-6OC46WQJ.js → chunk-55QETQZ3.js} +66 -35
  22. package/dist/{chunk-TQLRG35T.js → chunk-AD7CL772.js} +78 -45
  23. package/dist/chunk-CA6WCQ2T.js +54 -0
  24. package/dist/chunk-CPW6G62V.js +17 -0
  25. package/dist/{chunk-423S2KHN.js → chunk-FLAZ2JVN.js} +4 -2
  26. package/dist/{chunk-BH2F5WPN.js → chunk-GQI5UYXZ.js} +1 -1
  27. package/dist/{chunk-GYIVJXOJ.js → chunk-NA7OTBWW.js} +1 -1
  28. package/dist/{chunk-MDWB5HLO.js → chunk-NKNOXQ62.js} +1 -1
  29. package/dist/{chunk-3PKS2IIP.js → chunk-OX6TSVHY.js} +1 -1
  30. package/dist/{chunk-36RYTYT5.js → chunk-PD7KTSYN.js} +1 -1
  31. package/dist/chunk-S5GWAUG5.js +29 -0
  32. package/dist/{chunk-NDJOAITI.js → chunk-T3OROMAU.js} +9 -3
  33. package/dist/constants.cjs +1 -1
  34. package/dist/constants.js +1 -1
  35. package/dist/eventcatalog-config-file-utils.cjs +34 -41
  36. package/dist/eventcatalog-config-file-utils.d.cts +56 -58
  37. package/dist/eventcatalog-config-file-utils.d.ts +56 -58
  38. package/dist/eventcatalog-config-file-utils.js +1 -1
  39. package/dist/eventcatalog.cjs +531 -356
  40. package/dist/eventcatalog.config.d.cts +59 -1
  41. package/dist/eventcatalog.config.d.ts +59 -1
  42. package/dist/eventcatalog.js +63 -25
  43. package/dist/features.cjs +34 -41
  44. package/dist/features.js +2 -2
  45. package/dist/federation/federate.cjs +37 -44
  46. package/dist/federation/federate.js +2 -2
  47. package/dist/generate.cjs +37 -42
  48. package/dist/generate.js +4 -4
  49. package/dist/resolve-catalog-dependencies.cjs +34 -41
  50. package/dist/resolve-catalog-dependencies.js +2 -2
  51. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
  52. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
  53. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
  54. package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
  55. package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
  56. package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
  57. package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
  58. package/dist/runtime-dependencies/htmlparser2.mjs +3 -0
  59. package/dist/runtime-dependencies/manifest.json +13 -1
  60. package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
  61. package/dist/runtime-dependencies/react-syntax-highlighter__dist__cjs__create-element.js.mjs +3 -0
  62. package/dist/runtime-dependencies/y-protocols.mjs +3 -0
  63. package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
  64. package/dist/runtime-dependencies/yjs.mjs +3 -0
  65. package/dist/utils/child-process-signals.cjs +53 -0
  66. package/dist/utils/child-process-signals.d.cts +19 -0
  67. package/dist/utils/child-process-signals.d.ts +19 -0
  68. package/dist/utils/child-process-signals.js +6 -0
  69. package/dist/utils/cli-logger.cjs +1 -1
  70. package/dist/utils/cli-logger.js +2 -2
  71. package/dist/utils/license-status.cjs +78 -45
  72. package/dist/utils/license-status.d.cts +15 -5
  73. package/dist/utils/license-status.d.ts +15 -5
  74. package/dist/utils/license-status.js +1 -1
  75. package/eventcatalog/astro.config.mjs +23 -1
  76. package/eventcatalog/integrations/config-loader.mjs +56 -58
  77. package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
  78. package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
  79. package/eventcatalog/integrations/storage.mjs +20 -0
  80. package/eventcatalog/integrations/studio-server.mjs +32 -0
  81. package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
  82. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
  83. package/eventcatalog/src/components/Header.astro +3 -3
  84. package/eventcatalog/src/components/Homepage/CatalogDashboard.astro +342 -0
  85. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +60 -4
  86. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
  87. package/eventcatalog/src/components/NavigationProgress.astro +7 -0
  88. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +2 -0
  89. package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +2 -0
  90. package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +2 -0
  91. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +2 -0
  92. package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
  93. package/eventcatalog/src/components/Search/SearchModal.tsx +2 -10
  94. package/eventcatalog/src/components/Search/search-utils.ts +26 -0
  95. package/eventcatalog/src/components/Seo.astro +5 -1
  96. package/eventcatalog/src/components/Settings/AssistantSettingsForm.tsx +53 -48
  97. package/eventcatalog/src/components/Settings/GeneralSettingsForm.tsx +183 -178
  98. package/eventcatalog/src/components/Settings/LicenseSettings.tsx +140 -31
  99. package/eventcatalog/src/components/Settings/LlmAccessSettingsForm.tsx +40 -35
  100. package/eventcatalog/src/components/Settings/LogoUpload.tsx +1 -1
  101. package/eventcatalog/src/components/Settings/McpSettingsForm.tsx +9 -8
  102. package/eventcatalog/src/components/Settings/Row.tsx +7 -4
  103. package/eventcatalog/src/components/Settings/SettingsShared.tsx +3 -3
  104. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +53 -83
  105. package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
  106. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +131 -0
  107. package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
  108. package/eventcatalog/src/components/TrialBadge.astro +20 -9
  109. package/eventcatalog/src/content.config.ts +19 -2
  110. package/eventcatalog/src/features/auth/error.astro +4 -2
  111. package/eventcatalog/src/features/auth/login.astro +2 -1
  112. package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
  113. package/eventcatalog/src/features/auth/unauthorized.astro +1 -1
  114. package/eventcatalog/src/features/fields/pages/fields.astro +1 -1
  115. package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
  116. package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
  117. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
  118. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +24 -0
  119. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +278 -0
  120. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +272 -0
  121. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +65 -65
  122. package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
  123. package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
  124. package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
  125. package/eventcatalog/src/features/server/start.ts +25 -0
  126. package/eventcatalog/src/features/storage/database.ts +88 -0
  127. package/eventcatalog/src/features/storage/migrations.ts +85 -0
  128. package/eventcatalog/src/features/storage/sql/database.sql +5 -0
  129. package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
  130. package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
  131. package/eventcatalog/src/features/storage/sql.ts +28 -0
  132. package/eventcatalog/src/features/studio/agent-choreography.ts +384 -0
  133. package/eventcatalog/src/features/studio/agents.ts +28 -0
  134. package/eventcatalog/src/features/studio/api/canvases-api.ts +219 -0
  135. package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
  136. package/eventcatalog/src/features/studio/api/client.ts +40 -0
  137. package/eventcatalog/src/features/studio/canvas-actions.ts +540 -0
  138. package/eventcatalog/src/features/studio/canvas-preview.ts +52 -0
  139. package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
  140. package/eventcatalog/src/features/studio/catalog.ts +173 -0
  141. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +518 -0
  142. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +118 -0
  143. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +434 -0
  144. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +143 -0
  145. package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +65 -0
  146. package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
  147. package/eventcatalog/src/features/studio/components/JoinForm.tsx +109 -0
  148. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +206 -0
  149. package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
  150. package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
  151. package/eventcatalog/src/features/studio/components/Presence.tsx +293 -0
  152. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +396 -0
  153. package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
  154. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2091 -0
  155. package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
  156. package/eventcatalog/src/features/studio/document-name.ts +7 -0
  157. package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
  158. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +63 -0
  159. package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
  160. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +353 -0
  161. package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
  162. package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +109 -0
  163. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +782 -0
  164. package/eventcatalog/src/features/studio/identity.ts +59 -0
  165. package/eventcatalog/src/features/studio/limits.ts +2 -0
  166. package/eventcatalog/src/features/studio/pages/[id].astro +69 -0
  167. package/eventcatalog/src/features/studio/pages/index.astro +784 -0
  168. package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
  169. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +495 -0
  170. package/eventcatalog/src/features/studio/server/runtime.ts +498 -0
  171. package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
  172. package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
  173. package/eventcatalog/src/features/studio/server/storage.ts +90 -0
  174. package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
  175. package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
  176. package/eventcatalog/src/layouts/BaseLayout.astro +4 -4
  177. package/eventcatalog/src/layouts/SettingsLayout.astro +2 -2
  178. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +13 -5
  179. package/eventcatalog/src/pages/_index.astro +5 -342
  180. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +2 -3
  181. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +0 -7
  182. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +3 -1
  183. package/eventcatalog/src/pages/diagrams/[id]/[version]/_index.data.ts +0 -7
  184. package/eventcatalog/src/pages/diagrams/[id]/[version]/embed.astro +1 -0
  185. package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +1 -0
  186. package/eventcatalog/src/pages/directory/[type]/_index.data.ts +5 -7
  187. package/eventcatalog/src/pages/directory/[type]/index.astro +3 -1
  188. package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -7
  189. package/eventcatalog/src/pages/discover/[type]/index.astro +3 -1
  190. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/_index.data.ts +0 -7
  191. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +1 -0
  192. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/_index.data.ts +0 -7
  193. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +1 -0
  194. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +0 -7
  195. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +3 -1
  196. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/_[filename].data.ts +0 -7
  197. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +0 -7
  198. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +1 -0
  199. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +1 -0
  200. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/_[...filename].data.ts +0 -4
  201. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.ts +0 -7
  202. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/index.astro +1 -0
  203. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +3 -1
  204. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/_[filename].data.ts +0 -7
  205. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +1 -0
  206. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +2 -3
  207. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +0 -7
  208. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +1 -0
  209. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +1 -0
  210. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_[filename].data.ts +0 -7
  211. package/eventcatalog/src/pages/docs/[type]/[id]/_index.data.ts +0 -7
  212. package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +2 -1
  213. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/_index.data.ts +0 -7
  214. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +1 -0
  215. package/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +0 -7
  216. package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +1 -0
  217. package/eventcatalog/src/pages/docs/custom/[...path]/_index.data.ts +0 -7
  218. package/eventcatalog/src/pages/docs/custom/[...path]/index.astro +1 -0
  219. package/eventcatalog/src/pages/docs/services/[id]/[docType]/[docId]/index.astro +1 -1
  220. package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +1 -0
  221. package/eventcatalog/src/pages/docs/services/[id]/asyncapi/[filename].astro +1 -1
  222. package/eventcatalog/src/pages/docs/services/[id]/changelog/index.astro +1 -1
  223. package/eventcatalog/src/pages/docs/services/[id]/graphql/[filename].astro +1 -1
  224. package/eventcatalog/src/pages/docs/services/[id]/spec/[filename].astro +1 -1
  225. package/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts +0 -7
  226. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +1 -0
  227. package/eventcatalog/src/pages/docs/users/[id]/_index.data.ts +0 -7
  228. package/eventcatalog/src/pages/docs/users/[id]/index.astro +1 -0
  229. package/eventcatalog/src/pages/index.astro +4 -2
  230. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +0 -7
  231. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +1 -0
  232. package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +0 -7
  233. package/eventcatalog/src/pages/schemas/explorer/index.astro +4 -2
  234. package/eventcatalog/src/pages/settings/license.astro +9 -1
  235. package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/_index.data.ts +0 -7
  236. package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/index.astro +1 -0
  237. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +0 -7
  238. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts +0 -7
  239. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro +1 -0
  240. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/_index.data.ts +0 -7
  241. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +1 -0
  242. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -0
  243. package/eventcatalog/src/pages/visualiser/[type]/[id]/_index.data.ts +38 -0
  244. package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +18 -41
  245. package/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts +0 -7
  246. package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -1
  247. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/_index.data.ts +0 -7
  248. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +1 -0
  249. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/index.astro +1 -0
  250. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/_index.data.ts +0 -7
  251. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/index.astro +1 -0
  252. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/_index.data.ts +0 -7
  253. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/index.astro +1 -0
  254. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/index.astro +1 -0
  255. package/eventcatalog/src/types/react-syntax-highlighter.d.ts +10 -0
  256. package/eventcatalog/src/utils/collections/util.ts +8 -4
  257. package/eventcatalog/src/utils/feature.ts +8 -0
  258. package/eventcatalog/src/utils/git.ts +22 -5
  259. package/eventcatalog/src/utils/homepage-dashboard.ts +143 -0
  260. package/eventcatalog/src/utils/license.ts +1 -1
  261. package/eventcatalog/src/utils/link-validation.ts +25 -19
  262. package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +3 -1
  263. package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
  264. package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
  265. package/eventcatalog/src/utils/node-graphs/field-node-graph.ts +4 -2
  266. package/eventcatalog/src/utils/node-graphs/layout-node-graph.ts +20 -1
  267. package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
  268. package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
  269. package/eventcatalog/src/utils/page-loaders/hybrid-page.ts +4 -15
  270. package/eventcatalog/src/utils/page-title.ts +15 -0
  271. package/eventcatalog/src/utils/syntax-highlighter-renderer.ts +64 -0
  272. package/eventcatalog/src/utils/trial.ts +14 -1
  273. package/package.json +19 -13
  274. package/dist/runtime-dependencies/shelljs.mjs +0 -3
  275. package/eventcatalog/public/studio-bg.png +0 -0
  276. package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
  277. package/eventcatalog/src/pages/studio.astro +0 -316
@@ -0,0 +1,384 @@
1
+ import type { Edge, Node, XYPosition } from '@xyflow/react';
2
+ import type * as Y from 'yjs';
3
+ import {
4
+ addNodes,
5
+ connectNodes,
6
+ createThread,
7
+ deleteEdges,
8
+ deleteNodes,
9
+ fitContainersAround,
10
+ getCanvasMaps,
11
+ moveNode,
12
+ placeNode,
13
+ setEdgeRoutes,
14
+ setMeta,
15
+ type Author,
16
+ type CommentAnchor,
17
+ getAbsolutePosition,
18
+ sizeOf,
19
+ type LayoutResult,
20
+ getNodeName,
21
+ } from '@eventcatalog/visualiser/studio';
22
+ import {
23
+ describeNode,
24
+ nodeCenter,
25
+ planNodes,
26
+ resolveEdgeRefs,
27
+ topLeftFor,
28
+ updateCanvasNode,
29
+ type CatalogIndex,
30
+ type EdgeSpecs,
31
+ type NodeSpecs,
32
+ type NodeUpdate,
33
+ } from './canvas-actions';
34
+ import { getRelatedEdges } from './catalog';
35
+
36
+ /**
37
+ * Agents work on a canvas like a person would: their cursor travels to where they're going, they drop one node,
38
+ * drag a connection from it to the next, and so on, rather than everything appearing at once. Used by the MCP
39
+ * tools (on the server) and the WebMCP tools (in the browser), with a stage that makes the changes and shows
40
+ * the agent's presence there.
41
+ */
42
+
43
+ /** What everyone sees of an agent at work */
44
+ export type AgentPresence = {
45
+ pointer: XYPosition | null;
46
+ selection: string[];
47
+ activity?: string;
48
+ /** Where a connection being dragged starts: drawn from there to the pointer */
49
+ connecting?: XYPosition | null;
50
+ /** Nodes being moved, by id, and where they are (relative to their containers), until they're dropped */
51
+ moving?: Record<string, XYPosition> | null;
52
+ };
53
+
54
+ export type AgentStage = {
55
+ /** Change the canvas (and read it) */
56
+ change: <T>(fn: (doc: Y.Doc) => T) => Promise<T> | T;
57
+ /** Show the agent on the canvas */
58
+ present: (presence: AgentPresence) => void;
59
+ /** Where the agent's pointer was left last time, if it's been here */
60
+ lastPointer?: XYPosition | null;
61
+ /**
62
+ * Everyone sees the agent's presence, so nodes it moves are shown moving with it and only saved where they
63
+ * end up, like a person's drag. Otherwise each step of a move is saved, so others see it move.
64
+ */
65
+ sharesMoves?: boolean;
66
+ };
67
+
68
+ const FRAME_MS = 40;
69
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
70
+ const easeInOut = (t: number) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);
71
+ const distance = (a: XYPosition, b: XYPosition) => Math.hypot(a.x - b.x, a.y - b.y);
72
+
73
+ type Lookup = Map<string, Node>;
74
+ /** Where a node is on the canvas (nodes in containers are positioned relative to them) */
75
+ const positionOf = (node: Node, nodes: Lookup) => getAbsolutePosition(node, nodes);
76
+ /** Where connections leave a node (its right side) and arrive (its left side) */
77
+ const sourceHandle = (node: Node, nodes: Lookup) => {
78
+ const position = positionOf(node, nodes);
79
+ return { x: position.x + sizeOf(node).width, y: position.y + sizeOf(node).height / 2 };
80
+ };
81
+ const targetHandle = (node: Node, nodes: Lookup) => {
82
+ const position = positionOf(node, nodes);
83
+ return { x: position.x, y: position.y + sizeOf(node).height / 2 };
84
+ };
85
+ const nameOf = (node: Node) => getNodeName(node.type, node.data);
86
+
87
+ /**
88
+ * The agent's hand: moves its pointer smoothly (in frames, so it glides for everyone) and shows what it's doing.
89
+ * `pace` slows down or speeds up everything (big changes go faster, so a tool call doesn't take too long).
90
+ */
91
+ export function createAgentHand(stage: AgentStage, { pace = 1 }: { pace?: number } = {}) {
92
+ const presence: AgentPresence = { pointer: stage.lastPointer ?? null, selection: [] };
93
+ const show = () => stage.present({ ...presence });
94
+
95
+ const hand = {
96
+ get pointer() {
97
+ return presence.pointer;
98
+ },
99
+ async pause(ms: number) {
100
+ await sleep(ms * pace);
101
+ },
102
+ say(activity: string) {
103
+ presence.activity = activity;
104
+ show();
105
+ },
106
+ select(ids: string[]) {
107
+ presence.selection = ids;
108
+ show();
109
+ },
110
+ /**
111
+ * Glide the pointer to a point, e.g. dragging a connection from `connecting`, or carrying a node held at
112
+ * `grab` from its position
113
+ */
114
+ async moveTo(
115
+ target: XYPosition,
116
+ { connecting, carry }: { connecting?: XYPosition; carry?: { nodeId: string; grab: XYPosition } } = {}
117
+ ) {
118
+ // The first time, come in from a little way off rather than appearing on the spot
119
+ const from = presence.pointer ?? { x: target.x - 160, y: target.y - 120 };
120
+ const duration = Math.min(900, Math.max(250, 200 + distance(from, target) * 0.6)) * pace;
121
+ const frames = Math.max(1, Math.round(duration / FRAME_MS));
122
+ presence.connecting = connecting ?? null;
123
+ for (let frame = 1; frame <= frames; frame++) {
124
+ const t = easeInOut(frame / frames);
125
+ presence.pointer = { x: from.x + (target.x - from.x) * t, y: from.y + (target.y - from.y) * t };
126
+ if (carry)
127
+ presence.moving = {
128
+ [carry.nodeId]: { x: presence.pointer.x - carry.grab.x, y: presence.pointer.y - carry.grab.y },
129
+ };
130
+ show();
131
+ await sleep(FRAME_MS);
132
+ }
133
+ },
134
+ /** Let go of a connection being dragged, or nodes being carried (once where they're dropped is saved) */
135
+ release() {
136
+ presence.connecting = null;
137
+ presence.moving = null;
138
+ show();
139
+ },
140
+ };
141
+ return hand;
142
+ }
143
+
144
+ /** Faster for bigger changes, so a tool call stays well within client timeouts */
145
+ const paceFor = (steps: number) => Math.max(0.25, Math.min(1, 10 / Math.max(steps, 1)));
146
+
147
+ const readNodes = (doc: Y.Doc) => new Map<string, Node>(getCanvasMaps(doc).nodes.entries());
148
+
149
+ /** Drag a connection from one node to another, then make it */
150
+ async function playConnection(
151
+ stage: AgentStage,
152
+ hand: ReturnType<typeof createAgentHand>,
153
+ connect: (doc: Y.Doc) => Edge | { error: string },
154
+ sourceId: string,
155
+ targetId: string
156
+ ) {
157
+ const nodes = await stage.change(readNodes);
158
+ const source = nodes.get(sourceId);
159
+ const target = nodes.get(targetId);
160
+ if (!source || !target) return stage.change(connect);
161
+ hand.say(`Connecting ${nameOf(source)} to ${nameOf(target)}`);
162
+ const start = sourceHandle(source, nodes);
163
+ await hand.moveTo(start);
164
+ await hand.pause(120);
165
+ await hand.moveTo(targetHandle(target, nodes), { connecting: start });
166
+ const outcome = await stage.change(connect);
167
+ hand.release();
168
+ await hand.pause(180);
169
+ return outcome;
170
+ }
171
+
172
+ /** addToCanvas, played out: each node dropped in turn, connected (to related resources and as asked) as soon as it can be */
173
+ export async function playAddToCanvas(
174
+ stage: AgentStage,
175
+ { nodes: specs, edges = [] }: { nodes: NodeSpecs; edges?: EdgeSpecs },
176
+ catalog: CatalogIndex
177
+ ) {
178
+ const existing = await stage.change((doc) => [...readNodes(doc).values()]);
179
+ const { planned, errors } = planNodes(existing, specs, catalog, edges);
180
+ const pending = resolveEdgeRefs(edges, planned);
181
+ const hand = createAgentHand(stage, { pace: paceFor(planned.length * 2 + pending.length) });
182
+ const connections: string[] = [];
183
+
184
+ for (const { node: plannedNode, canvasPosition, name, resource } of planned) {
185
+ hand.say(`Adding ${name}`);
186
+ const { width, height } = sizeOf(plannedNode);
187
+ await hand.moveTo({ x: canvasPosition.x + width / 2, y: canvasPosition.y + height / 2 });
188
+ await hand.pause(150);
189
+ const { node, related } = await stage.change((doc) => {
190
+ const onCanvas = readNodes(doc);
191
+ // Where it goes on the canvas, relative to its container as it is now (containers move as they grow)
192
+ const parent = plannedNode.parentId ? onCanvas.get(plannedNode.parentId) : undefined;
193
+ const origin = parent ? getAbsolutePosition(parent, onCanvas) : { x: 0, y: 0 };
194
+ const node = { ...plannedNode, position: { x: canvasPosition.x - origin.x, y: canvasPosition.y - origin.y } };
195
+ addNodes(doc, [node]);
196
+ // The containers it's in grow to fit it
197
+ fitContainersAround(doc, node.id);
198
+ const related = resource
199
+ ? getRelatedEdges(resource, node.id, [...onCanvas.values()], catalog.relationsByKey, node.type)
200
+ : [];
201
+ return { node, related };
202
+ });
203
+ hand.select([node.id]);
204
+ await hand.pause(300);
205
+
206
+ // Its connections to what's already there: related catalog resources, then the ones asked for
207
+ for (const edge of related) {
208
+ await playConnection(stage, hand, (doc) => (addNodes(doc, [], [edge]), edge), edge.source, edge.target);
209
+ connections.push(edge.id);
210
+ }
211
+ const onCanvas = await stage.change((doc) => new Set(readNodes(doc).keys()));
212
+ for (const edge of pending.filter((edge) => onCanvas.has(edge.from) && onCanvas.has(edge.to))) {
213
+ pending.splice(pending.indexOf(edge), 1);
214
+ const outcome = await playConnection(
215
+ stage,
216
+ hand,
217
+ (doc) => connectNodes(doc, { source: edge.from, target: edge.to }, edge.label),
218
+ edge.from,
219
+ edge.to
220
+ );
221
+ if ('error' in outcome) errors.push(outcome.error);
222
+ else connections.push(outcome.id);
223
+ }
224
+ }
225
+
226
+ // Connections to nodes that aren't on the canvas
227
+ for (const edge of pending) {
228
+ const outcome = await stage.change((doc) => connectNodes(doc, { source: edge.from, target: edge.to }, edge.label));
229
+ if ('error' in outcome) errors.push(outcome.error);
230
+ }
231
+
232
+ hand.say('Done');
233
+ return { created: planned.map(({ ref, node, name }) => ({ ref, nodeId: node.id, name })), connections, errors };
234
+ }
235
+
236
+ /** connectCanvasNodes, played out: each connection dragged from one node to the other */
237
+ export async function playConnectAll(stage: AgentStage, edges: EdgeSpecs) {
238
+ const hand = createAgentHand(stage, { pace: paceFor(edges.length) });
239
+ const connections: string[] = [];
240
+ const errors: string[] = [];
241
+ for (const edge of edges) {
242
+ const outcome = await playConnection(
243
+ stage,
244
+ hand,
245
+ (doc) => connectNodes(doc, { source: edge.from, target: edge.to }, edge.label),
246
+ edge.from,
247
+ edge.to
248
+ );
249
+ if ('error' in outcome) errors.push(outcome.error);
250
+ else connections.push(outcome.id);
251
+ }
252
+ return { connections, errors };
253
+ }
254
+
255
+ /** Moving a node, played out: grab it and drag it there */
256
+ export async function playMoveNode(stage: AgentStage, nodeId: string, to: XYPosition, hand = createAgentHand(stage)) {
257
+ const nodes = await stage.change(readNodes);
258
+ const node = nodes.get(nodeId);
259
+ if (!node) return false;
260
+ // Positions are relative to the node's container, the pointer is on the canvas
261
+ const parent = node.parentId ? nodes.get(node.parentId) : undefined;
262
+ const origin = parent ? positionOf(parent, nodes) : { x: 0, y: 0 };
263
+ const grab = { x: origin.x + sizeOf(node).width / 2, y: origin.y + 24 };
264
+ hand.say(`Moving ${nameOf(node)}`);
265
+ await hand.moveTo({ x: node.position.x + grab.x, y: node.position.y + grab.y });
266
+ hand.select([nodeId]);
267
+ await hand.pause(120);
268
+ if (stage.sharesMoves) {
269
+ await hand.moveTo({ x: to.x + grab.x, y: to.y + grab.y }, { carry: { nodeId, grab } });
270
+ await stage.change((doc) => moveNode(doc, nodeId, to));
271
+ hand.release();
272
+ return true;
273
+ }
274
+ const from = node.position;
275
+ const frames = 12;
276
+ for (let frame = 1; frame <= frames; frame++) {
277
+ const t = easeInOut(frame / frames);
278
+ const position = { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t };
279
+ await stage.change((doc) => moveNode(doc, nodeId, position));
280
+ await hand.moveTo({ x: position.x + grab.x, y: position.y + grab.y });
281
+ }
282
+ return true;
283
+ }
284
+
285
+ /**
286
+ * Laying the canvas out: everything moves in one change, and everyone's canvas glides the nodes into place
287
+ * (the layout time in the canvas's meta tells them to)
288
+ */
289
+ export async function playLayout(stage: AgentStage, layout: LayoutResult) {
290
+ const hand = createAgentHand(stage);
291
+ hand.say('Tidying the layout');
292
+ await stage.change((doc) => applyLayout(doc, layout));
293
+ await hand.pause(500);
294
+ hand.say('Done');
295
+ }
296
+
297
+ /**
298
+ * Moves nodes, resizes containers and routes connections from a layout, in one change, and marks when it
299
+ * happened. Connections the layout didn't route lose their old route (drawn as a plain step instead).
300
+ */
301
+ export const applyLayout = (doc: Y.Doc, layout: LayoutResult) =>
302
+ doc.transact(() => {
303
+ layout.nodes.forEach(({ position, size }, id) => placeNode(doc, id, position, size));
304
+ setEdgeRoutes(doc, layout.routes);
305
+ setMeta(doc, { layoutAt: Date.now() });
306
+ });
307
+
308
+ /** Removing things, played out: point at each, then remove it */
309
+ export async function playRemove(stage: AgentStage, nodeIds: string[], connectionIds: string[]) {
310
+ const hand = createAgentHand(stage, { pace: paceFor(nodeIds.length) });
311
+ if (connectionIds.length) await stage.change((doc) => deleteEdges(doc, connectionIds));
312
+ for (const id of nodeIds) {
313
+ const nodes = await stage.change(readNodes);
314
+ const node = nodes.get(id);
315
+ if (!node) continue;
316
+ hand.say(`Removing ${nameOf(node)}`);
317
+ await hand.moveTo(nodeCenter(node, nodes));
318
+ hand.select([id]);
319
+ await hand.pause(250);
320
+ await stage.change((doc) => deleteNodes(doc, [id]));
321
+ hand.select([]);
322
+ }
323
+ }
324
+
325
+ /** Commenting, played out: go to where the comment goes, then leave it */
326
+ export async function playComment(
327
+ stage: AgentStage,
328
+ { text, nodeId, x, y }: { text: string; nodeId?: string; x?: number; y?: number },
329
+ author: Author
330
+ ) {
331
+ const nodes = await stage.change(readNodes);
332
+ const node = nodeId ? nodes.get(nodeId) : undefined;
333
+ if (nodeId && !node) return { error: `No node "${nodeId}" on the canvas` };
334
+ const offset = node ? { x: sizeOf(node).width - 8, y: 8 } : undefined;
335
+ const position = node && positionOf(node, nodes);
336
+ const anchor: CommentAnchor =
337
+ node && offset && position
338
+ ? { position: { x: position.x + offset.x, y: position.y + offset.y }, nodeId: node.id, offset }
339
+ : { position: { x: x ?? 0, y: y ?? 0 } };
340
+ const hand = createAgentHand(stage);
341
+ hand.say('Commenting');
342
+ await hand.moveTo(anchor.position);
343
+ await hand.pause(400);
344
+ const threadId = await stage.change((doc) => createThread(doc, anchor, text, author));
345
+ return { threadId };
346
+ }
347
+
348
+ /** Pointing at a node while changing it (e.g. a rename) */
349
+ export async function playOnNode<T>(stage: AgentStage, nodeId: string, activity: string, change: (doc: Y.Doc) => T) {
350
+ const nodes = await stage.change(readNodes);
351
+ const node = nodes.get(nodeId);
352
+ const hand = createAgentHand(stage);
353
+ if (node) {
354
+ hand.say(activity);
355
+ await hand.moveTo(nodeCenter(node, nodes));
356
+ hand.select([nodeId]);
357
+ await hand.pause(250);
358
+ }
359
+ return stage.change(change);
360
+ }
361
+
362
+ /** Updating a node, played out: point at it to rename or re-describe it, then drag it to its new centre (x/y) */
363
+ export async function playUpdateNode(
364
+ stage: AgentStage,
365
+ { nodeId, name, summary, version, x, y }: NodeUpdate & { x?: number; y?: number }
366
+ ) {
367
+ const missing = { error: `No node "${nodeId}" on the canvas` };
368
+ if (name !== undefined || summary !== undefined || version !== undefined) {
369
+ const outcome = await playOnNode(stage, nodeId, 'Editing', (doc) =>
370
+ updateCanvasNode(doc, { nodeId, name, summary, version })
371
+ );
372
+ if ('error' in outcome) return outcome;
373
+ }
374
+ if (x !== undefined || y !== undefined) {
375
+ const nodes = await stage.change(readNodes);
376
+ const node = nodes.get(nodeId);
377
+ if (!node) return missing;
378
+ const center = nodeCenter(node, nodes);
379
+ await playMoveNode(stage, nodeId, topLeftFor(node, { x: x ?? center.x, y: y ?? center.y }, nodes));
380
+ }
381
+ const nodes = await stage.change(readNodes);
382
+ const node = nodes.get(nodeId);
383
+ return node ? describeNode(node, nodes) : missing;
384
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Who an agent is, named after its client when it doesn't say, in its brand's colour: on the server (MCP tools) and
3
+ * in a chat's canvas view (which joins as the chat's agent). Their logos are in the visualiser's ClientLogos.
4
+ */
5
+ export type AgentIdentity = { name: string; color: string };
6
+
7
+ const AGENT_COLORS: Record<string, string> = {
8
+ ChatGPT: '#10a37f',
9
+ Codex: '#10a37f',
10
+ Claude: '#d97757',
11
+ 'Claude Code': '#d97757',
12
+ Cursor: '#111827',
13
+ };
14
+
15
+ /** Agents' clients, by what's in their user agent or host name (most specific first) */
16
+ const CLIENTS: [RegExp, string][] = [
17
+ [/claude[-_ ]?code/i, 'Claude Code'],
18
+ [/claude|anthropic/i, 'Claude'],
19
+ [/codex/i, 'Codex'],
20
+ [/openai|chatgpt/i, 'ChatGPT'],
21
+ [/cursor/i, 'Cursor'],
22
+ ];
23
+
24
+ /** Who the agent is, from the name it gives, else its client's user agent (or a chat's host name) */
25
+ export const getAgent = (agentName: string | undefined, client = ''): AgentIdentity => {
26
+ const name = agentName?.trim() || CLIENTS.find(([pattern]) => pattern.test(client))?.[1] || 'AI agent';
27
+ return { name, color: AGENT_COLORS[name] ?? '#8b5cf6' };
28
+ };
@@ -0,0 +1,219 @@
1
+ import { Hono, type Context } from 'hono';
2
+ import { z } from 'zod';
3
+ import { paginate } from '@features/tools/catalog-tools';
4
+ import { type CanvasContent, type CanvasStatus } from '@eventcatalog/visualiser/studio';
5
+ import { MAX_TITLE_LENGTH } from '../limits';
6
+ import { diagramLevelsSchema, indexCatalog, newCanvasEdgesSchema, nodeSpecsSchema, planCanvas } from '../canvas-actions';
7
+ import { getCatalogResources } from '../catalog-resources';
8
+ import { MAX_NAME_LENGTH } from '../identity';
9
+ import type { CanvasSummary, StudioRuntime } from '../server/runtime';
10
+ import type { SignedInUser } from '../server/sign-in';
11
+
12
+ /**
13
+ * The Studio API: canvases as a whole (listing, creating, reading and deleting them). What's on a canvas is changed
14
+ * over the collaboration connection, or by agents through the MCP tools, never here: they're live documents that
15
+ * people and agents edit together.
16
+ *
17
+ * GET /api/studio/canvases Canvases, most recently changed first (?pageSize=, ?cursor= for the next page)
18
+ * (not ones left empty and untitled: GET /:id still finds them)
19
+ * POST /api/studio/canvases Creates one: { "title"?: string, "createdBy"?: string, "nodes"?: [...],
20
+ * "edges"?: [...] }, starting with the nodes and edges given, if any (as the
21
+ * add_to_canvas MCP tool takes them: catalog resources or new components, x/y
22
+ * their centres, and only the connections in edges are made, each drawn
23
+ * along its "route" if given: { "points": [{x, y}, ...], "label"?: {x, y} }),
24
+ * and "levels" (L1 and L2 laid out, e.g. as a diagram shows them, by the
25
+ * nodes' refs), shown as given until what's on the canvas changes
26
+ * GET /api/studio/canvases/:id One canvas
27
+ * POST /api/studio/canvases/:id/copy Copies its design (nodes and connections, not comments or status) to a new
28
+ * draft: { "title"?: string, "createdBy"?: string }
29
+ * DELETE /api/studio/canvases/:id Deletes it for everyone (anyone who has it open is told), for good
30
+ *
31
+ * Errors are `{ "error": "..." }` with the HTTP status. With sign-in on, only signed-in people can use it (401
32
+ * otherwise), and canvases are created by whoever is signed in. Requests that change something (POST, DELETE) from
33
+ * outside a browser send `Content-Type: application/json`: Astro refuses ones with no content type that don't come
34
+ * from the catalog's own pages.
35
+ */
36
+ export const CANVASES_API_PATH = '/api/studio/canvases';
37
+
38
+ const CANVAS_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
39
+ const MAX_PAGE_SIZE = 100;
40
+
41
+ /** A canvas, as the API gives it */
42
+ export type ApiCanvas = {
43
+ id: string;
44
+ title: string | null;
45
+ status: CanvasStatus;
46
+ /** ISO 8601 */
47
+ createdAt: string | null;
48
+ /** Who started it (a person, or an agent) */
49
+ createdBy: string | null;
50
+ /** When anyone (a person, or an agent) last changed it (ISO 8601) */
51
+ updatedAt: string | null;
52
+ nodeCount: number;
53
+ edgeCount: number;
54
+ openComments: number;
55
+ /** Where people open it */
56
+ url: string;
57
+ };
58
+
59
+ export type CanvasesApiEnv = {
60
+ Bindings: {
61
+ runtime: StudioRuntime;
62
+ /** Who's signed in (sign-in on) */
63
+ signedInAs?: SignedInUser;
64
+ /** A canvas's page */
65
+ canvasUrl: (canvasId: string) => string;
66
+ };
67
+ };
68
+
69
+ const MAX_NODES = 1000;
70
+ const MAX_EDGES = 2000;
71
+
72
+ const NewCanvas = z.object({
73
+ title: z.string().trim().max(MAX_TITLE_LENGTH).optional(),
74
+ // Ignored when someone's signed in: it's them
75
+ createdBy: z.string().trim().max(MAX_NAME_LENGTH).optional(),
76
+ });
77
+ // A new canvas can start with nodes and edges; a copy starts with the original's
78
+ const NewCanvasWithContent = NewCanvas.extend({
79
+ nodes: nodeSpecsSchema.max(MAX_NODES).optional(),
80
+ edges: newCanvasEdgesSchema.max(MAX_EDGES).optional(),
81
+ levels: diagramLevelsSchema.optional(),
82
+ }).refine((canvas) => !canvas.edges?.length || canvas.nodes, {
83
+ message: 'Edges connect the nodes given with them: give nodes too',
84
+ path: ['edges'],
85
+ });
86
+ const ListQuery = z.object({
87
+ cursor: z.string().optional(),
88
+ pageSize: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).optional(),
89
+ });
90
+
91
+ const iso = (time?: number) => (time ? new Date(time).toISOString() : null);
92
+ const toApiCanvas = (canvas: CanvasSummary, canvasUrl: (canvasId: string) => string): ApiCanvas => ({
93
+ id: canvas.canvasId,
94
+ title: canvas.title ?? null,
95
+ status: canvas.status,
96
+ createdAt: iso(canvas.createdAt),
97
+ createdBy: canvas.createdBy ?? null,
98
+ updatedAt: iso(canvas.updatedAt),
99
+ nodeCount: canvas.nodeCount,
100
+ edgeCount: canvas.edgeCount,
101
+ openComments: canvas.openComments,
102
+ url: canvasUrl(canvas.canvasId),
103
+ });
104
+ const describeIssues = (error: z.ZodError) =>
105
+ error.issues.map((issue) => (issue.path.length ? `${issue.path.join('.')}: ${issue.message}` : issue.message)).join('; ');
106
+
107
+ type ApiContext = Context<CanvasesApiEnv>;
108
+ const noCanvas = (c: ApiContext, id: string) => c.json({ error: `No canvas "${id}"` }, 404);
109
+
110
+ type NewCanvasDetails = { title?: string; createdBy?: string };
111
+
112
+ /** The request's JSON body (or {} without one), or the response saying what's wrong with it */
113
+ const readJson = async (c: ApiContext): Promise<unknown> => {
114
+ // JSON only: a page on another site can send a form or plain text here without asking first, but not JSON
115
+ const body = await c.req.text();
116
+ if (body && !c.req.header('content-type')?.toLowerCase().startsWith('application/json')) {
117
+ return c.json({ error: 'Send the canvas as JSON (Content-Type: application/json)' }, 415);
118
+ }
119
+ try {
120
+ return body ? JSON.parse(body) : {};
121
+ } catch {
122
+ return c.json({ error: 'The body is not valid JSON' }, 400);
123
+ }
124
+ };
125
+
126
+ const detailsOf = (c: ApiContext, canvas: z.output<typeof NewCanvas>): NewCanvasDetails => ({
127
+ title: canvas.title || undefined,
128
+ // Whoever's signed in, if anyone
129
+ createdBy: c.env.signedInAs?.name ?? (canvas.createdBy || undefined),
130
+ });
131
+
132
+ /** A copy's details from the request (optional), or the response saying what's wrong with them */
133
+ const readCopy = async (c: ApiContext): Promise<NewCanvasDetails | Response> => {
134
+ const json = await readJson(c);
135
+ if (json instanceof Response) return json;
136
+ const parsed = NewCanvas.safeParse(json);
137
+ if (!parsed.success) return c.json({ error: describeIssues(parsed.error) }, 400);
138
+ return detailsOf(c, parsed.data);
139
+ };
140
+
141
+ /** A new canvas's details and what it starts with (all optional), or the response saying what's wrong with them */
142
+ const readNewCanvas = async (c: ApiContext): Promise<{ details: NewCanvasDetails; content?: CanvasContent } | Response> => {
143
+ const json = await readJson(c);
144
+ if (json instanceof Response) return json;
145
+ const parsed = NewCanvasWithContent.safeParse(json);
146
+ if (!parsed.success) return c.json({ error: describeIssues(parsed.error) }, 400);
147
+ const details = detailsOf(c, parsed.data);
148
+ if (!parsed.data.nodes) return { details };
149
+ const { resources, relations } = await getCatalogResources();
150
+ const { content, errors } = planCanvas(
151
+ parsed.data.nodes,
152
+ parsed.data.edges ?? [],
153
+ indexCatalog(resources, relations),
154
+ parsed.data.levels
155
+ );
156
+ if (errors.length) return c.json({ error: errors.join('; ') }, 400);
157
+ return { details, content };
158
+ };
159
+
160
+ /** The API, served under `basePath` (the catalog's base URL, then CANVASES_API_PATH) */
161
+ export const createCanvasesApi = (basePath: string) => {
162
+ const app = new Hono<CanvasesApiEnv>({ strict: false }).basePath(basePath);
163
+
164
+ app.get('/', (c) => {
165
+ const query = ListQuery.safeParse(c.req.query());
166
+ if (!query.success) return c.json({ error: describeIssues(query.error) }, 400);
167
+ const { runtime, canvasUrl } = c.env;
168
+ const page = paginate(runtime.listCanvases(), query.data.cursor, query.data.pageSize);
169
+ if ('error' in page) return c.json({ error: page.error }, 400);
170
+ return c.json({
171
+ canvases: page.items.map((canvas) => toApiCanvas(canvas, canvasUrl)),
172
+ totalCount: page.totalCount,
173
+ ...(page.nextCursor && { nextCursor: page.nextCursor }),
174
+ });
175
+ });
176
+
177
+ /** A canvas just made, as created */
178
+ const created = (c: ApiContext, canvasId: string) => {
179
+ c.header('Location', `${basePath}/${canvasId}`);
180
+ return c.json({ canvas: toApiCanvas(c.env.runtime.getCanvas(canvasId)!, c.env.canvasUrl) }, 201);
181
+ };
182
+
183
+ app.post('/', async (c) => {
184
+ const canvas = await readNewCanvas(c);
185
+ if (canvas instanceof Response) return canvas;
186
+ return created(c, await c.env.runtime.createCanvas(canvas.details, canvas.content));
187
+ });
188
+
189
+ app.post('/:id/copy', async (c) => {
190
+ const id = c.req.param('id');
191
+ if (!CANVAS_ID.test(id)) return noCanvas(c, id);
192
+ const details = await readCopy(c);
193
+ if (details instanceof Response) return details;
194
+ const copyId = await c.env.runtime.copyCanvas(id, details);
195
+ return copyId ? created(c, copyId) : noCanvas(c, id);
196
+ });
197
+
198
+ app.get('/:id', (c) => {
199
+ const id = c.req.param('id');
200
+ const canvas = CANVAS_ID.test(id) ? c.env.runtime.getCanvas(id) : undefined;
201
+ if (!canvas) return noCanvas(c, id);
202
+ return c.json({ canvas: toApiCanvas(canvas, c.env.canvasUrl) });
203
+ });
204
+
205
+ app.delete('/:id', (c) => {
206
+ const id = c.req.param('id');
207
+ if (!CANVAS_ID.test(id)) return noCanvas(c, id);
208
+ try {
209
+ if (!c.env.runtime.deleteCanvas(id)) return noCanvas(c, id);
210
+ } catch (error) {
211
+ console.error(`[studio] Could not delete canvas ${id}`, error);
212
+ return c.json({ error: `Could not delete the canvas: ${(error as Error).message}` }, 500);
213
+ }
214
+ return c.body(null, 204);
215
+ });
216
+
217
+ app.notFound((c) => c.json({ error: 'Not found' }, 404));
218
+ return app;
219
+ };
@@ -0,0 +1,25 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { buildUrl } from '@utils/url-builder';
3
+ import { startStudio } from '../server/runtime';
4
+ import { getSignedInUser, isSignedIn } from '../server/sign-in';
5
+ import { CANVASES_API_PATH, createCanvasesApi } from './canvases-api';
6
+
7
+ // The Studio API (see canvases-api.ts)
8
+ const api = createCanvasesApi(buildUrl(CANVASES_API_PATH, true));
9
+
10
+ export const ALL: APIRoute = async ({ request, locals, url }) => {
11
+ let runtime: Awaited<ReturnType<typeof startStudio>>;
12
+ try {
13
+ runtime = await startStudio({ isSignedIn });
14
+ } catch (error) {
15
+ // Its storage can't be used (e.g. the database was updated by a newer EventCatalog)
16
+ return Response.json({ error: `Studio isn't available: ${(error as Error).message}` }, { status: 503 });
17
+ }
18
+ return api.fetch(request, {
19
+ runtime,
20
+ signedInAs: getSignedInUser(locals),
21
+ canvasUrl: (canvasId: string) => `${url.origin}${buildUrl(`/studio/${canvasId}`)}`,
22
+ });
23
+ };
24
+
25
+ export const prerender = false;