@ecsplico/editor 1.0.0 → 2.1.0

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 (363) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +7 -7
  3. package/dist/adapters/anchors.d.ts +80 -0
  4. package/dist/adapters/anchors.js +270 -0
  5. package/dist/adapters/anchors.js.map +1 -0
  6. package/dist/adapters/comments/client.d.ts +64 -0
  7. package/dist/adapters/comments/client.js +536 -0
  8. package/dist/adapters/comments/client.js.map +1 -0
  9. package/dist/adapters/comments/drizzle.d.ts +430 -0
  10. package/dist/adapters/comments/drizzle.js +236 -0
  11. package/dist/adapters/comments/drizzle.js.map +1 -0
  12. package/dist/adapters/comments/errors.d.ts +9 -0
  13. package/dist/adapters/comments/errors.js +19 -0
  14. package/dist/adapters/comments/errors.js.map +1 -0
  15. package/dist/adapters/comments/json.d.ts +19 -0
  16. package/dist/adapters/comments/json.js +94 -0
  17. package/dist/adapters/comments/json.js.map +1 -0
  18. package/dist/adapters/comments/record.js +78 -0
  19. package/dist/adapters/comments/record.js.map +1 -0
  20. package/dist/adapters/comments/server.d.ts +23 -0
  21. package/dist/adapters/comments/server.js +102 -0
  22. package/dist/adapters/comments/server.js.map +1 -0
  23. package/dist/adapters/comments/types.d.ts +109 -0
  24. package/dist/adapters/documents.js +108 -0
  25. package/dist/adapters/documents.js.map +1 -0
  26. package/dist/adapters/drizzle.d.ts +293 -0
  27. package/dist/adapters/drizzle.js +157 -0
  28. package/dist/adapters/drizzle.js.map +1 -0
  29. package/dist/adapters/files.js +30 -0
  30. package/dist/adapters/files.js.map +1 -0
  31. package/dist/adapters/forge.d.ts +58 -0
  32. package/dist/adapters/forge.js +91 -0
  33. package/dist/adapters/forge.js.map +1 -0
  34. package/dist/adapters/index.d.ts +13 -0
  35. package/dist/adapters/index.js +0 -0
  36. package/dist/adapters/json.d.ts +24 -0
  37. package/dist/adapters/json.js +128 -0
  38. package/dist/adapters/json.js.map +1 -0
  39. package/dist/adapters/nitro.d.ts +61 -0
  40. package/dist/adapters/nitro.js +212 -0
  41. package/dist/adapters/nitro.js.map +1 -0
  42. package/dist/adapters/routes.js +16 -0
  43. package/dist/adapters/routes.js.map +1 -0
  44. package/dist/adapters/shared.d.ts +37 -0
  45. package/dist/adapters/shared.js +32 -0
  46. package/dist/adapters/shared.js.map +1 -0
  47. package/dist/adapters/vue.d.ts +67 -0
  48. package/dist/adapters/vue.js +187 -0
  49. package/dist/adapters/vue.js.map +1 -0
  50. package/dist/code/CodeSurface.vue.d.ts +3 -3
  51. package/dist/core/doc-anchors.d.ts +5 -0
  52. package/dist/core/doc-anchors.js +4 -3
  53. package/dist/core/doc-anchors.js.map +1 -1
  54. package/dist/core/editor-ui.js +57 -1
  55. package/dist/core/editor-ui.js.map +1 -1
  56. package/dist/core/notify.d.ts +1 -1
  57. package/dist/core/notify.js.map +1 -1
  58. package/dist/core/surface-session.js +139 -0
  59. package/dist/core/surface-session.js.map +1 -0
  60. package/dist/document/DocumentSurface.d.ts +1 -1
  61. package/dist/document/DocumentSurface.vue.d.ts +3 -3
  62. package/dist/features/ai/agent-op-review.d.ts +2 -2
  63. package/dist/features/ai/agent-op-review.js.map +1 -1
  64. package/dist/features/ai/agent-ops.d.ts +1 -1
  65. package/dist/features/ai/agent-ops.js +9 -6
  66. package/dist/features/ai/agent-ops.js.map +1 -1
  67. package/dist/features/ai/ai-apply.d.ts +1 -1
  68. package/dist/features/ai/ai-review.js +10 -1
  69. package/dist/features/ai/ai-review.js.map +1 -1
  70. package/dist/features/ai/components/AiSelectionMenu.js +78 -252
  71. package/dist/features/ai/components/AiSelectionMenu.js.map +1 -1
  72. package/dist/features/ai/components/AiSelectionMenu.vue.d.ts +2 -1
  73. package/dist/features/ai/components/AiSidebar.js +2 -2
  74. package/dist/features/ai/components/DocumentChatAnswer.js +1 -1
  75. package/dist/features/ai/components/DocumentChatOps.js +1 -0
  76. package/dist/features/ai/components/DocumentChatOps.js.map +1 -1
  77. package/dist/features/ai/components/ModelQuickSwitch.js +1 -1
  78. package/dist/features/ai/components/SettingsAiTab.js +1 -1
  79. package/dist/features/ai/components/SettingsModelProfiles.js +1 -1
  80. package/dist/features/ai/document-chat.d.ts +3 -2
  81. package/dist/features/ai/document-chat.js +16 -19
  82. package/dist/features/ai/document-chat.js.map +1 -1
  83. package/dist/features/ai/index.d.ts +5 -3
  84. package/dist/features/ai/index.js +4 -2
  85. package/dist/features/ai/protocol/agent-ops.d.ts +22 -17
  86. package/dist/features/ai/protocol/agent-ops.js +65 -39
  87. package/dist/features/ai/protocol/agent-ops.js.map +1 -1
  88. package/dist/features/ai/protocol/block-lines.d.ts +89 -0
  89. package/dist/features/ai/protocol/block-lines.js +177 -0
  90. package/dist/features/ai/protocol/block-lines.js.map +1 -0
  91. package/dist/features/ai/protocol/chat.js +10 -10
  92. package/dist/features/ai/protocol/chat.js.map +1 -1
  93. package/dist/features/ai/protocol/index.d.ts +3 -2
  94. package/dist/features/ai/protocol/index.js +3 -2
  95. package/dist/features/ai/react/AiSelectionMenu.d.ts +3 -2
  96. package/dist/features/ai/react/AiSelectionMenu.js +64 -255
  97. package/dist/features/ai/react/AiSelectionMenu.js.map +1 -1
  98. package/dist/features/ai/react/AiSidebar.js +2 -2
  99. package/dist/features/ai/react/DocumentChatAnswer.js +1 -1
  100. package/dist/features/ai/react/DocumentChatOps.js +1 -0
  101. package/dist/features/ai/react/DocumentChatOps.js.map +1 -1
  102. package/dist/features/ai/react/ModelQuickSwitch.js +1 -1
  103. package/dist/features/ai/react/SettingsAiTab.js +1 -1
  104. package/dist/features/ai/react/SettingsModelProfiles.js +1 -1
  105. package/dist/features/ai/react/use-ai-selection-flow.js +34 -0
  106. package/dist/features/ai/react/use-ai-selection-flow.js.map +1 -0
  107. package/dist/features/ai/selection-flow.d.ts +133 -0
  108. package/dist/features/ai/selection-flow.js +283 -0
  109. package/dist/features/ai/selection-flow.js.map +1 -0
  110. package/dist/features/ai/use-ai-assistant.d.ts +1 -1
  111. package/dist/features/ai/use-document-chat.d.ts +1 -1
  112. package/dist/features/authors/components/AuthorColorsPanel.js +1 -1
  113. package/dist/features/authors/feature.js +1 -1
  114. package/dist/features/authors/index.js +1 -1
  115. package/dist/features/authors/react/AuthorColorsPanel.js +1 -1
  116. package/dist/features/comments/components/CommentBubble.js +1 -1
  117. package/dist/features/comments/components/CommentComposer.js +1 -1
  118. package/dist/features/comments/components/CommentThreadView.js +5 -2
  119. package/dist/features/comments/components/CommentThreadView.js.map +1 -1
  120. package/dist/features/comments/components/CommentsMargin.js +108 -230
  121. package/dist/features/comments/components/CommentsMargin.js.map +1 -1
  122. package/dist/features/comments/components/CommentsOverlay.js +1 -1
  123. package/dist/features/comments/feature.js +1 -1
  124. package/dist/features/comments/index.d.ts +4 -2
  125. package/dist/features/comments/index.js +4 -2
  126. package/dist/features/comments/margin-controller.d.ts +44 -0
  127. package/dist/features/comments/margin-controller.js +132 -0
  128. package/dist/features/comments/margin-controller.js.map +1 -0
  129. package/dist/features/comments/margin-layout.d.ts +55 -0
  130. package/dist/features/comments/margin-layout.js +79 -0
  131. package/dist/features/comments/margin-layout.js.map +1 -0
  132. package/dist/features/comments/model.d.ts +8 -0
  133. package/dist/features/comments/model.js +60 -34
  134. package/dist/features/comments/model.js.map +1 -1
  135. package/dist/features/comments/react/CommentBubble.js +1 -1
  136. package/dist/features/comments/react/CommentComposer.js +1 -1
  137. package/dist/features/comments/react/CommentThreadView.js +2 -2
  138. package/dist/features/comments/react/CommentThreadView.js.map +1 -1
  139. package/dist/features/comments/react/CommentsMargin.d.ts +0 -5
  140. package/dist/features/comments/react/CommentsMargin.js +85 -251
  141. package/dist/features/comments/react/CommentsMargin.js.map +1 -1
  142. package/dist/features/comments/react/CommentsOverlay.js +1 -1
  143. package/dist/features/comments/react/use-comments.js +1 -1
  144. package/dist/features/comments/react/use-margin-layout.js +45 -0
  145. package/dist/features/comments/react/use-margin-layout.js.map +1 -0
  146. package/dist/features/comments/react.d.ts +1 -1
  147. package/dist/features/comments/schema.d.ts +23 -0
  148. package/dist/features/comments/schema.js.map +1 -1
  149. package/dist/features/comments/snapshot-equality.js +40 -0
  150. package/dist/features/comments/snapshot-equality.js.map +1 -0
  151. package/dist/features/comments/thread-anchor.js +33 -0
  152. package/dist/features/comments/thread-anchor.js.map +1 -0
  153. package/dist/features/comments/use-comments.d.ts +3 -1
  154. package/dist/features/comments/use-comments.js +1 -0
  155. package/dist/features/comments/use-comments.js.map +1 -1
  156. package/dist/features/comments/use-margin-layout.js +29 -0
  157. package/dist/features/comments/use-margin-layout.js.map +1 -0
  158. package/dist/features/comments/vue.d.ts +1 -1
  159. package/dist/features/composites/accepted-children.d.ts +21 -0
  160. package/dist/features/composites/accepted-children.js +126 -0
  161. package/dist/features/composites/accepted-children.js.map +1 -0
  162. package/dist/features/composites/components/CompositeDetails.js +1 -1
  163. package/dist/features/composites/components/CompositeInspector.js +38 -164
  164. package/dist/features/composites/components/CompositeInspector.js.map +1 -1
  165. package/dist/features/composites/feature.d.ts +2 -1
  166. package/dist/features/composites/feature.js +8 -2
  167. package/dist/features/composites/feature.js.map +1 -1
  168. package/dist/features/composites/index.d.ts +5 -3
  169. package/dist/features/composites/index.js +8 -6
  170. package/dist/features/composites/inspector-model.d.ts +97 -0
  171. package/dist/features/composites/inspector-model.js +255 -0
  172. package/dist/features/composites/inspector-model.js.map +1 -0
  173. package/dist/features/composites/kinds.d.ts +11 -0
  174. package/dist/features/composites/kinds.js +17 -2
  175. package/dist/features/composites/kinds.js.map +1 -1
  176. package/dist/features/composites/outline.d.ts +18 -5
  177. package/dist/features/composites/outline.js +44 -12
  178. package/dist/features/composites/outline.js.map +1 -1
  179. package/dist/features/composites/react/CompositeDetails.js +1 -1
  180. package/dist/features/composites/react/CompositeInspector.d.ts +1 -1
  181. package/dist/features/composites/react/CompositeInspector.js +21 -180
  182. package/dist/features/composites/react/CompositeInspector.js.map +1 -1
  183. package/dist/features/composites/react/CompositeStatusMenu.d.ts +1 -1
  184. package/dist/features/composites/react/CompositeView.js +1 -1
  185. package/dist/features/composites/react/use-composite-inspector.js +29 -0
  186. package/dist/features/composites/react/use-composite-inspector.js.map +1 -0
  187. package/dist/features/composites/schema.d.ts +2 -2
  188. package/dist/features/composites/schema.js +2 -2
  189. package/dist/features/composites/schema.js.map +1 -1
  190. package/dist/features/composites/use-composite-anchors.d.ts +1 -1
  191. package/dist/features/composites/use-composite-inspector.js +41 -0
  192. package/dist/features/composites/use-composite-inspector.js.map +1 -0
  193. package/dist/features/composites/use-composite-rows.d.ts +1 -1
  194. package/dist/features/media/index.d.ts +1 -1
  195. package/dist/features/media/vue.d.ts +1 -1
  196. package/dist/features/mentions/index.d.ts +3 -3
  197. package/dist/features/mentions/index.js +3 -3
  198. package/dist/features/mentions/schema.d.ts +1 -1
  199. package/dist/features/mentions/schema.js +1 -1
  200. package/dist/features/themes/index.d.ts +1 -1
  201. package/dist/features/themes/index.js +1 -1
  202. package/dist/features/transcript/feature.js +1 -1
  203. package/dist/features/transcript/index.js +2 -2
  204. package/dist/i18n/de/ai.js +2 -0
  205. package/dist/i18n/de/ai.js.map +1 -1
  206. package/dist/i18n/de/common.js +1 -2
  207. package/dist/i18n/de/common.js.map +1 -1
  208. package/dist/i18n/de/composites.js +8 -1
  209. package/dist/i18n/de/composites.js.map +1 -1
  210. package/dist/i18n/de/tree.js +14 -1
  211. package/dist/i18n/de/tree.js.map +1 -1
  212. package/dist/i18n/de.d.ts +22 -1
  213. package/dist/i18n/en/ai.js +2 -0
  214. package/dist/i18n/en/ai.js.map +1 -1
  215. package/dist/i18n/en/common.js +1 -2
  216. package/dist/i18n/en/common.js.map +1 -1
  217. package/dist/i18n/en/composites.js +8 -1
  218. package/dist/i18n/en/composites.js.map +1 -1
  219. package/dist/i18n/en/tree.js +14 -1
  220. package/dist/i18n/en/tree.js.map +1 -1
  221. package/dist/integrations/composite-ai/composite-fill.js +2 -2
  222. package/dist/integrations/composite-ai/composite-summary.js +2 -2
  223. package/dist/integrations/composite-ai/index.js +2 -2
  224. package/dist/integrations/transcript-ai/transcript-cleanup.js +1 -1
  225. package/dist/markdown/MarkdownSurface.d.ts +1 -1
  226. package/dist/markdown/MarkdownSurface.js +2 -2
  227. package/dist/markdown/MarkdownSurface.vue.d.ts +7 -7
  228. package/dist/markdown/index.d.ts +2 -2
  229. package/dist/nuxt/module.d.ts +0 -6
  230. package/dist/nuxt/module.js +3 -5
  231. package/dist/nuxt/module.js.map +1 -1
  232. package/dist/nuxt/runtime/install-context.d.ts +2 -3
  233. package/dist/nuxt/runtime/install-context.js +2 -3
  234. package/dist/nuxt/runtime/install-context.js.map +1 -1
  235. package/dist/nuxt/runtime/plugins/editor-context-nuxt-ui.client.ts +1 -1
  236. package/dist/nuxt/runtime/server/routes/collab/[id].js +1 -1
  237. package/dist/react/EditorSurface.d.ts +1 -1
  238. package/dist/react/EditorSurface.js +43 -149
  239. package/dist/react/EditorSurface.js.map +1 -1
  240. package/dist/react/EditorToolbar.js +3 -40
  241. package/dist/react/EditorToolbar.js.map +1 -1
  242. package/dist/react/styles/ui-vars.css +2 -2
  243. package/dist/schema/advanced.js +2 -2
  244. package/dist/server/adapter.d.ts +2 -2
  245. package/dist/server/collab.js +1 -1
  246. package/dist/server/handlers/ai.d.ts +4 -2
  247. package/dist/server/handlers/ai.js +11 -9
  248. package/dist/server/handlers/ai.js.map +1 -1
  249. package/dist/server/handlers/context.d.ts +1 -1
  250. package/dist/server/handlers/editor.d.ts +1 -1
  251. package/dist/server/handlers/editor.js +2 -2
  252. package/dist/server/host.d.ts +1 -1
  253. package/dist/server/index.d.ts +2 -2
  254. package/dist/settings/index.d.ts +1 -1
  255. package/dist/styles/ai.css +1 -1
  256. package/dist/styles/authors.css +1 -1
  257. package/dist/styles/block-ui.css +1 -1
  258. package/dist/styles/collab.css +1 -1
  259. package/dist/styles/comments.css +1 -1
  260. package/dist/styles/core.css +2 -2
  261. package/dist/styles/media.css +1 -1
  262. package/dist/styles/mentions.css +1 -1
  263. package/dist/styles/themes.css +1 -1
  264. package/dist/styles/tokens.css +1 -1
  265. package/dist/styles/transcript.css +1 -1
  266. package/dist/tree/DocumentTree.js +159 -387
  267. package/dist/tree/DocumentTree.js.map +1 -1
  268. package/dist/tree/drop-rules.js +37 -0
  269. package/dist/tree/drop-rules.js.map +1 -0
  270. package/dist/tree/react/DocumentTree.d.ts +2 -2
  271. package/dist/tree/react/DocumentTree.js +135 -397
  272. package/dist/tree/react/DocumentTree.js.map +1 -1
  273. package/dist/tree/react/use-tree-model.js +129 -0
  274. package/dist/tree/react/use-tree-model.js.map +1 -0
  275. package/dist/tree/react.js +1 -1
  276. package/dist/tree/tree-host.js +100 -0
  277. package/dist/tree/tree-host.js.map +1 -0
  278. package/dist/tree/tree-items.js +22 -43
  279. package/dist/tree/tree-items.js.map +1 -1
  280. package/dist/tree/tree-model.js +580 -0
  281. package/dist/tree/tree-model.js.map +1 -0
  282. package/dist/tree/tree-rows.js +77 -0
  283. package/dist/tree/tree-rows.js.map +1 -0
  284. package/dist/tree/use-tree-model.js +94 -0
  285. package/dist/tree/use-tree-model.js.map +1 -0
  286. package/dist/tree/vue.js +1 -1
  287. package/dist/vue/AdvancedEditor.vue.d.ts +4 -4
  288. package/dist/vue/SimpleEditor.vue.d.ts +2 -2
  289. package/dist/vue/components/EditorSurface.js +27 -87
  290. package/dist/vue/components/EditorSurface.js.map +1 -1
  291. package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
  292. package/dist/vue/components/EditorToolbar.js +7 -44
  293. package/dist/vue/components/EditorToolbar.js.map +1 -1
  294. package/dist/vue/context.d.ts +4 -4
  295. package/dist/vue/context.js +3 -2
  296. package/dist/vue/context.js.map +1 -1
  297. package/dist/vue/kit/types.d.ts +1 -1
  298. package/dist/vue/kit/use-ui-kit.d.ts +2 -2
  299. package/dist/vue/kit/use-ui-kit.js +3 -3
  300. package/dist/vue/kit/use-ui-kit.js.map +1 -1
  301. package/dist/vue/use-editor-ui.d.ts +1 -1
  302. package/dist/vue/use-notifier.d.ts +1 -1
  303. package/docs/integration.md +98 -5
  304. package/docs/llms.md +10 -2
  305. package/docs/usage.md +10 -5
  306. package/package.json +70 -23
  307. package/dist/nuxt/runtime/plugins/editor-context-primevue.client.ts +0 -8
  308. package/dist/vue/kit/primevue/Alert.js +0 -49
  309. package/dist/vue/kit/primevue/Alert.js.map +0 -1
  310. package/dist/vue/kit/primevue/Avatar.js +0 -54
  311. package/dist/vue/kit/primevue/Avatar.js.map +0 -1
  312. package/dist/vue/kit/primevue/Badge.js +0 -41
  313. package/dist/vue/kit/primevue/Badge.js.map +0 -1
  314. package/dist/vue/kit/primevue/Button.js +0 -112
  315. package/dist/vue/kit/primevue/Button.js.map +0 -1
  316. package/dist/vue/kit/primevue/Chip.js +0 -38
  317. package/dist/vue/kit/primevue/Chip.js.map +0 -1
  318. package/dist/vue/kit/primevue/DropdownMenu.js +0 -98
  319. package/dist/vue/kit/primevue/DropdownMenu.js.map +0 -1
  320. package/dist/vue/kit/primevue/FieldGroup.js +0 -22
  321. package/dist/vue/kit/primevue/FieldGroup.js.map +0 -1
  322. package/dist/vue/kit/primevue/FormField.js +0 -56
  323. package/dist/vue/kit/primevue/FormField.js.map +0 -1
  324. package/dist/vue/kit/primevue/Icon.js +0 -23
  325. package/dist/vue/kit/primevue/Icon.js.map +0 -1
  326. package/dist/vue/kit/primevue/Input.js +0 -96
  327. package/dist/vue/kit/primevue/Input.js.map +0 -1
  328. package/dist/vue/kit/primevue/InputMenu.js +0 -122
  329. package/dist/vue/kit/primevue/InputMenu.js.map +0 -1
  330. package/dist/vue/kit/primevue/Modal.js +0 -59
  331. package/dist/vue/kit/primevue/Modal.js.map +0 -1
  332. package/dist/vue/kit/primevue/Popover.js +0 -135
  333. package/dist/vue/kit/primevue/Popover.js.map +0 -1
  334. package/dist/vue/kit/primevue/RadioGroup.js +0 -58
  335. package/dist/vue/kit/primevue/RadioGroup.js.map +0 -1
  336. package/dist/vue/kit/primevue/Select.js +0 -68
  337. package/dist/vue/kit/primevue/Select.js.map +0 -1
  338. package/dist/vue/kit/primevue/SelectMenu.js +0 -94
  339. package/dist/vue/kit/primevue/SelectMenu.js.map +0 -1
  340. package/dist/vue/kit/primevue/Separator.js +0 -16
  341. package/dist/vue/kit/primevue/Separator.js.map +0 -1
  342. package/dist/vue/kit/primevue/Switch.js +0 -46
  343. package/dist/vue/kit/primevue/Switch.js.map +0 -1
  344. package/dist/vue/kit/primevue/Table.js +0 -54
  345. package/dist/vue/kit/primevue/Table.js.map +0 -1
  346. package/dist/vue/kit/primevue/Tabs.js +0 -49
  347. package/dist/vue/kit/primevue/Tabs.js.map +0 -1
  348. package/dist/vue/kit/primevue/Textarea.js +0 -55
  349. package/dist/vue/kit/primevue/Textarea.js.map +0 -1
  350. package/dist/vue/kit/primevue/Tooltip.js +0 -43
  351. package/dist/vue/kit/primevue/Tooltip.js.map +0 -1
  352. package/dist/vue/kit/primevue/Tree.js +0 -75
  353. package/dist/vue/kit/primevue/Tree.js.map +0 -1
  354. package/dist/vue/kit/primevue/select-options.js +0 -55
  355. package/dist/vue/kit/primevue/select-options.js.map +0 -1
  356. package/dist/vue/kit/primevue/shared.js +0 -49
  357. package/dist/vue/kit/primevue/shared.js.map +0 -1
  358. package/dist/vue/kit/primevue/trigger.js +0 -25
  359. package/dist/vue/kit/primevue/trigger.js.map +0 -1
  360. package/dist/vue/kit/primevue.css +0 -86
  361. package/dist/vue/kit/primevue.d.ts +0 -33
  362. package/dist/vue/kit/primevue.js +0 -96
  363. package/dist/vue/kit/primevue.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drizzle.js","names":[],"sources":["../../../src/adapters/comments/drizzle.ts"],"sourcesContent":["import { and, eq, type SQL, sql } from 'drizzle-orm'\nimport { integer, primaryKey, type SQLiteColumn, type SQLiteTable, sqliteTable, text } from 'drizzle-orm/sqlite-core'\nimport type { PortableAnchor } from '../anchors'\nimport type { EditorDb } from '../drizzle'\nimport { conflict, invalid, notFound } from './errors'\nimport { assertNewThread } from './record'\nimport type { Annotation, Comment, CommentStore, CommentThread, Discussion } from './types'\n\nconst COMMENT_TABLES = {\n annotations: sqliteTable('editor_comment_annotations', {\n documentId: text('document_id').notNull(),\n id: text('id').notNull(),\n threadId: text('thread_id').notNull(),\n seq: integer('seq').notNull(),\n anchor: text('anchor', { mode: 'json' }).$type<PortableAnchor>().notNull(),\n }, table => [primaryKey({ columns: [table.documentId, table.id] })]),\n threads: sqliteTable('editor_comment_threads', {\n documentId: text('document_id').notNull(),\n id: text('id').notNull(),\n annotationId: text('annotation_id').notNull(),\n status: text('status', { enum: ['open', 'resolved'] }).notNull(),\n seq: integer('seq').notNull(),\n }, table => [primaryKey({ columns: [table.documentId, table.id] })]),\n comments: sqliteTable('editor_comments', {\n documentId: text('document_id').notNull(),\n threadId: text('thread_id').notNull(),\n id: text('id').notNull(),\n seq: integer('seq').notNull(),\n body: text('body').notNull(),\n authorId: text('author_id').notNull(),\n authorName: text('author_name'),\n authorColor: text('author_color'),\n createdAt: text('created_at').notNull(),\n editedAt: text('edited_at'),\n }, table => [primaryKey({ columns: [table.documentId, table.threadId, table.id] })]),\n}\n\nexport type CommentTables = typeof COMMENT_TABLES\n\n/**\n * The tables of the comment store in a SQLite database:\n *\n * editor_comment_annotations(document_id, id, thread_id, seq, anchor json; PK (document_id, id))\n * editor_comment_threads(document_id, id, annotation_id, status, seq; PK (document_id, id))\n * editor_comments(document_id, thread_id, id, seq, body, author_id, author_name?, author_color?, created_at, edited_at?; PK (document_id, thread_id, id))\n *\n * `seq` orders rows within their document (annotations, threads) or thread (comments). Call it from the host's drizzle\n * schema (so `drizzle-kit` migrates the tables) or use `bootstrapCommentTables` in a host without migrations.\n */\nexport const commentTables = (): CommentTables => COMMENT_TABLES\n\n/** `CREATE TABLE IF NOT EXISTS` for the tables of `commentTables()`. */\nexport async function bootstrapCommentTables(db: EditorDb): Promise<void> {\n await db.run(sql`CREATE TABLE IF NOT EXISTS editor_comment_annotations (document_id text NOT NULL, id text NOT NULL, thread_id text NOT NULL, seq integer NOT NULL, anchor text NOT NULL, PRIMARY KEY (document_id, id))`)\n await db.run(sql`CREATE TABLE IF NOT EXISTS editor_comment_threads (document_id text NOT NULL, id text NOT NULL, annotation_id text NOT NULL, status text NOT NULL, seq integer NOT NULL, PRIMARY KEY (document_id, id))`)\n await db.run(sql`CREATE TABLE IF NOT EXISTS editor_comments (document_id text NOT NULL, thread_id text NOT NULL, id text NOT NULL, seq integer NOT NULL, body text NOT NULL, author_id text NOT NULL, author_name text, author_color text, created_at text NOT NULL, edited_at text, PRIMARY KEY (document_id, thread_id, id))`)\n}\n\nexport interface DrizzleCommentStoreOptions {\n db: EditorDb\n tables: CommentTables\n}\n\n/** Writers on one database of one process run one after another: SQLite has one writer, and each operation is several statements. */\nconst queues = new WeakMap<object, Promise<unknown>>()\n\nfunction serialized<T>(db: object, action: () => Promise<T>): Promise<T> {\n const run = (queues.get(db) ?? Promise.resolve()).then(action)\n const tail = run.catch(() => {})\n queues.set(db, tail)\n void tail.then(() => {\n if (queues.get(db) === tail) queues.delete(db)\n })\n return run\n}\n\n/**\n * The comment store on drizzle tables. Operations are several single statements (no transaction, so it runs on sync and\n * async drivers) applied one after another per database; a failed `createThread` removes what it had written. A\n * unique key still protects against a second process writing the same ids. Unlike the JSON store it keeps only the\n * fields of `Comment`, `Annotation` and `CommentThread`.\n */\nexport function createDrizzleCommentStore({ db, tables }: DrizzleCommentStoreOptions): CommentStore {\n const { annotations, threads, comments } = tables\n type CommentRow = typeof comments.$inferSelect\n\n const commentOf = (row: CommentRow): Comment => ({\n id: row.id,\n body: row.body,\n authorId: row.authorId,\n createdAt: row.createdAt,\n ...(row.authorName !== null && { authorName: row.authorName }),\n ...(row.authorColor !== null && { authorColor: row.authorColor }),\n ...(row.editedAt !== null && { editedAt: row.editedAt }),\n })\n\n async function threadRow(documentId: string, id: string) {\n return db.select().from(threads).where(and(eq(threads.documentId, documentId), eq(threads.id, id))).get()\n }\n\n async function threadOf(documentId: string, id: string): Promise<CommentThread | undefined> {\n const row = await threadRow(documentId, id)\n if (!row) return undefined\n const rows = await db.select().from(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, id))).orderBy(comments.seq).all()\n return { id: row.id, annotationId: row.annotationId, status: row.status, comments: rows.map(commentOf) }\n }\n\n async function requireThread(documentId: string, id: string): Promise<CommentThread> {\n const thread = await threadOf(documentId, id)\n if (!thread) throw notFound('comment thread', id)\n return thread\n }\n\n async function discussion(documentId: string): Promise<Discussion> {\n const [annotationRows, threadRows, commentRows] = await Promise.all([\n db.select().from(annotations).where(eq(annotations.documentId, documentId)).orderBy(annotations.seq).all(),\n db.select().from(threads).where(eq(threads.documentId, documentId)).orderBy(threads.seq).all(),\n db.select().from(comments).where(eq(comments.documentId, documentId)).orderBy(comments.seq).all(),\n ])\n return {\n annotations: annotationRows.map(row => ({ id: row.id, threadId: row.threadId, anchor: row.anchor })),\n threads: threadRows.map(row => ({\n id: row.id,\n annotationId: row.annotationId,\n status: row.status,\n comments: commentRows.filter(comment => comment.threadId === row.id).map(commentOf),\n })),\n }\n }\n\n const nextSeq = async (column: SQLiteColumn, table: SQLiteTable, where: SQL | undefined): Promise<number> => {\n const row = await db.select({ value: sql<number>`coalesce(max(${column}), 0)` }).from(table).where(where).get()\n return (row?.value ?? 0) + 1\n }\n\n const insertComment = async (documentId: string, threadId: string, comment: Comment) => {\n await db.insert(comments).values({\n documentId,\n threadId,\n id: comment.id,\n seq: await nextSeq(comments.seq, comments, and(eq(comments.documentId, documentId), eq(comments.threadId, threadId))),\n body: comment.body,\n authorId: comment.authorId,\n authorName: comment.authorName ?? null,\n authorColor: comment.authorColor ?? null,\n createdAt: comment.createdAt,\n editedAt: comment.editedAt ?? null,\n }).run()\n }\n\n const write = <T>(action: () => Promise<T>) => serialized(db, action)\n\n return {\n getDiscussion: discussion,\n getThread: (documentId, threadId) => threadOf(documentId, threadId),\n async listThreads(documentId) {\n return (await discussion(documentId)).threads\n },\n\n createThread: (documentId, input) => write(async () => {\n assertNewThread(input)\n const { annotation, thread } = input\n if (await db.select({ id: annotations.id }).from(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).get()) throw conflict(`Annotation \"${annotation.id}\" already exists.`)\n if (await threadRow(documentId, thread.id)) throw conflict(`Comment thread \"${thread.id}\" already exists.`)\n try {\n await db.insert(annotations).values({\n documentId,\n id: annotation.id,\n threadId: annotation.threadId,\n seq: await nextSeq(annotations.seq, annotations, eq(annotations.documentId, documentId)),\n anchor: annotation.anchor,\n }).run()\n await db.insert(threads).values({\n documentId,\n id: thread.id,\n annotationId: thread.annotationId,\n status: 'open',\n seq: await nextSeq(threads.seq, threads, eq(threads.documentId, documentId)),\n }).run()\n await insertComment(documentId, thread.id, thread.initialComment)\n }\n catch (error) {\n await db.delete(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, thread.id))).run()\n await db.delete(threads).where(and(eq(threads.documentId, documentId), eq(threads.id, thread.id))).run()\n await db.delete(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).run()\n throw error\n }\n return { annotation: structuredClone(annotation), thread: await requireThread(documentId, thread.id) }\n }),\n\n addComment: (documentId, threadId, comment) => write(async () => {\n await requireThread(documentId, threadId)\n if (await db.select({ id: comments.id }).from(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId), eq(comments.id, comment.id))).get()) {\n throw conflict(`Comment \"${comment.id}\" already exists in thread \"${threadId}\".`)\n }\n await insertComment(documentId, threadId, comment)\n return requireThread(documentId, threadId)\n }),\n\n editComment: (documentId, threadId, commentId, body, editedAt) => write(async () => {\n const thread = await requireThread(documentId, threadId)\n if (!thread.comments.some(comment => comment.id === commentId)) throw notFound('comment', commentId)\n await db.update(comments).set({ body, editedAt }).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId), eq(comments.id, commentId))).run()\n return requireThread(documentId, threadId)\n }),\n\n removeComment: (documentId, threadId, commentId) => write(async () => {\n const thread = await requireThread(documentId, threadId)\n const index = thread.comments.findIndex(comment => comment.id === commentId)\n if (index < 0) throw notFound('comment', commentId)\n if (index === 0) throw invalid('The first comment of a thread cannot be removed on its own; remove the thread.')\n await db.delete(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId), eq(comments.id, commentId))).run()\n return requireThread(documentId, threadId)\n }),\n\n setThreadStatus: (documentId, threadId, status) => write(async () => {\n await requireThread(documentId, threadId)\n await db.update(threads).set({ status }).where(and(eq(threads.documentId, documentId), eq(threads.id, threadId))).run()\n return requireThread(documentId, threadId)\n }),\n\n updateAnchor: (documentId, annotation: Annotation) => write(async () => {\n const row = await db.select().from(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).get()\n if (!row) throw notFound('annotation', annotation.id)\n if (row.threadId !== annotation.threadId) throw invalid('An annotation cannot move to another thread.')\n await db.update(annotations).set({ anchor: annotation.anchor }).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).run()\n return structuredClone(annotation)\n }),\n\n removeThread: (documentId, threadId) => write(async () => {\n await db.delete(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId))).run()\n await db.delete(threads).where(and(eq(threads.documentId, documentId), eq(threads.id, threadId))).run()\n await db.delete(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.threadId, threadId))).run()\n }),\n\n removeDocuments: prefix => write(async () => {\n const under = (column: { name: string }) => sql`substr(${sql.identifier(column.name)}, 1, ${prefix.length}) = ${prefix}`\n const documents = new Set<string>()\n for (const [table, column] of [[annotations, annotations.documentId], [threads, threads.documentId], [comments, comments.documentId]] as const) {\n for (const row of await db.selectDistinct({ documentId: column }).from(table).where(under(column)).all()) documents.add(row.documentId)\n }\n for (const [table, column] of [[comments, comments.documentId], [threads, threads.documentId], [annotations, annotations.documentId]] as const) {\n await db.delete(table).where(under(column)).run()\n }\n return documents.size\n }),\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ //#region src/adapters/comments/errors.d.ts
2
+ /** A caller mistake in a comment store operation; `status` is the HTTP status `handleCommentsRequest` answers with. */
3
+ export declare class CommentStoreError extends Error {
4
+ readonly code: 'not-found' | 'conflict' | 'invalid';
5
+ readonly status: 404 | 409 | 400;
6
+ constructor(message: string, code: 'not-found' | 'conflict' | 'invalid', status: 404 | 409 | 400);
7
+ }
8
+ //#endregion
9
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1,19 @@
1
+ //#region src/adapters/comments/errors.ts
2
+ /** A caller mistake in a comment store operation; `status` is the HTTP status `handleCommentsRequest` answers with. */
3
+ var CommentStoreError = class extends Error {
4
+ code;
5
+ status;
6
+ constructor(message, code, status) {
7
+ super(message);
8
+ this.code = code;
9
+ this.status = status;
10
+ this.name = "CommentStoreError";
11
+ }
12
+ };
13
+ const notFound = (what, id) => new CommentStoreError(`Unknown ${what} "${id}".`, "not-found", 404);
14
+ const conflict = (message) => new CommentStoreError(message, "conflict", 409);
15
+ const invalid = (message) => new CommentStoreError(message, "invalid", 400);
16
+ //#endregion
17
+ export { CommentStoreError, conflict, invalid, notFound };
18
+
19
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../../src/adapters/comments/errors.ts"],"sourcesContent":["/** A caller mistake in a comment store operation; `status` is the HTTP status `handleCommentsRequest` answers with. */\nexport class CommentStoreError extends Error {\n constructor(message: string, readonly code: 'not-found' | 'conflict' | 'invalid', readonly status: 404 | 409 | 400) {\n super(message)\n this.name = 'CommentStoreError'\n }\n}\n\nexport const notFound = (what: string, id: string) => new CommentStoreError(`Unknown ${what} \"${id}\".`, 'not-found', 404)\nexport const conflict = (message: string) => new CommentStoreError(message, 'conflict', 409)\nexport const invalid = (message: string) => new CommentStoreError(message, 'invalid', 400)\n"],"mappings":";;AACA,IAAa,oBAAb,cAAuC,MAAM;CACL;CAAqD;CAA3F,YAAY,SAAiB,MAAqD,QAAkC;EAClH,MAAM,OAAO;EADuB,KAAA,OAAA;EAAqD,KAAA,SAAA;EAEzF,KAAK,OAAO;CACd;AACF;AAEA,MAAa,YAAY,MAAc,OAAe,IAAI,kBAAkB,WAAW,KAAK,IAAI,GAAG,KAAK,aAAa,GAAG;AACxH,MAAa,YAAY,YAAoB,IAAI,kBAAkB,SAAS,YAAY,GAAG;AAC3F,MAAa,WAAW,YAAoB,IAAI,kBAAkB,SAAS,WAAW,GAAG"}
@@ -0,0 +1,19 @@
1
+ import { CommentStore } from "./types.js";
2
+ //#region src/adapters/comments/json.d.ts
3
+ export interface JsonCommentStoreOptions {
4
+ /** The one file all documents' discussions live in, e.g. `<data>/concept-discussions.json`; its directory is created on demand. */
5
+ file: string;
6
+ }
7
+ /**
8
+ * Comment threads in one JSON file, the layout of skaile-forge's `concept-discussions.json`:
9
+ *
10
+ * { "<documentId>": { "annotations": [{ id, threadId, anchor }], "threads": [{ id, annotationId, status, comments }] } }
11
+ *
12
+ * written with two-space indentation and a trailing newline. Existing files load unchanged and fields this package does
13
+ * not know stay on the records it rewrites; a change that alters nothing does not touch the file. Every write goes
14
+ * through a temp file and a rename and through one queue per file, so concurrent operations (also of two stores on the
15
+ * same file) apply one after another. Single node only: the file is not coordinated across processes.
16
+ */
17
+ export declare function createJsonCommentStore(options: JsonCommentStoreOptions): CommentStore;
18
+ //#endregion
19
+ //# sourceMappingURL=json.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { isMissing, writeAtomic } from "../files.js";
2
+ import { addComment, createThread, editComment, emptyDiscussion, removeComment, removeThread, setThreadStatus, updateAnchor } from "./record.js";
3
+ import { promises } from "node:fs";
4
+ import path from "node:path";
5
+ //#region src/adapters/comments/json.ts
6
+ /** Stores of one process on one file share a queue, so two of them cannot read the same snapshot and lose one write. */
7
+ const queues = /* @__PURE__ */ new Map();
8
+ function serialized(file, action) {
9
+ const run = (queues.get(file) ?? Promise.resolve()).then(action);
10
+ const tail = run.catch(() => {});
11
+ queues.set(file, tail);
12
+ tail.then(() => {
13
+ if (queues.get(file) === tail) queues.delete(file);
14
+ });
15
+ return run;
16
+ }
17
+ async function read(file) {
18
+ let raw;
19
+ try {
20
+ raw = await promises.readFile(file, "utf8");
21
+ } catch (error) {
22
+ if (isMissing(error)) return { all: {} };
23
+ throw error;
24
+ }
25
+ const parsed = JSON.parse(raw);
26
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new TypeError(`@ecsplico/editor: ${file} does not hold a JSON object of discussions`);
27
+ return { all: parsed };
28
+ }
29
+ /** `record` with the two lists every operation relies on (records written by older tools may lack one). */
30
+ function complete(record) {
31
+ if (!record) return void 0;
32
+ record.annotations ??= [];
33
+ record.threads ??= [];
34
+ return record;
35
+ }
36
+ /**
37
+ * Comment threads in one JSON file, the layout of skaile-forge's `concept-discussions.json`:
38
+ *
39
+ * { "<documentId>": { "annotations": [{ id, threadId, anchor }], "threads": [{ id, annotationId, status, comments }] } }
40
+ *
41
+ * written with two-space indentation and a trailing newline. Existing files load unchanged and fields this package does
42
+ * not know stay on the records it rewrites; a change that alters nothing does not touch the file. Every write goes
43
+ * through a temp file and a rename and through one queue per file, so concurrent operations (also of two stores on the
44
+ * same file) apply one after another. Single node only: the file is not coordinated across processes.
45
+ */
46
+ function createJsonCommentStore(options) {
47
+ const file = path.resolve(options.file);
48
+ /** Reads the file, runs `operation` on the document's record and writes the result back when the text changed. `create`: a document without a record gets one. */
49
+ function mutate(documentId, create, operation) {
50
+ return serialized(file, async () => {
51
+ const { all } = await read(file);
52
+ const existing = complete(all[documentId]);
53
+ const before = JSON.stringify(all);
54
+ const record = existing ?? emptyDiscussion();
55
+ const result = operation(record);
56
+ if (existing || create) all[documentId] = record;
57
+ if (JSON.stringify(all) !== before) await writeAtomic(file, `${JSON.stringify(all, null, 2)}\n`);
58
+ return structuredClone(result);
59
+ });
60
+ }
61
+ async function discussion(documentId) {
62
+ const { all } = await read(file);
63
+ return structuredClone(complete(all[documentId]) ?? emptyDiscussion());
64
+ }
65
+ return {
66
+ getDiscussion: discussion,
67
+ async getThread(documentId, threadId) {
68
+ return (await discussion(documentId)).threads.find((thread) => thread.id === threadId);
69
+ },
70
+ async listThreads(documentId) {
71
+ return (await discussion(documentId)).threads;
72
+ },
73
+ createThread: (documentId, input) => mutate(documentId, true, (record) => createThread(record, input)),
74
+ addComment: (documentId, threadId, comment) => mutate(documentId, false, (record) => addComment(record, threadId, comment)),
75
+ editComment: (documentId, threadId, commentId, body, editedAt) => mutate(documentId, false, (record) => editComment(record, threadId, commentId, body, editedAt)),
76
+ removeComment: (documentId, threadId, commentId) => mutate(documentId, false, (record) => removeComment(record, threadId, commentId)),
77
+ setThreadStatus: (documentId, threadId, status) => mutate(documentId, false, (record) => setThreadStatus(record, threadId, status)),
78
+ updateAnchor: (documentId, annotation) => mutate(documentId, false, (record) => updateAnchor(record, annotation)),
79
+ removeThread: (documentId, threadId) => mutate(documentId, false, (record) => removeThread(record, threadId)),
80
+ removeDocuments: (prefix) => serialized(file, async () => {
81
+ const { all } = await read(file);
82
+ const kept = {};
83
+ let removed = 0;
84
+ for (const [documentId, record] of Object.entries(all)) if (documentId.startsWith(prefix)) removed++;
85
+ else kept[documentId] = record;
86
+ if (removed) await writeAtomic(file, `${JSON.stringify(kept, null, 2)}\n`);
87
+ return removed;
88
+ })
89
+ };
90
+ }
91
+ //#endregion
92
+ export { createJsonCommentStore };
93
+
94
+ //# sourceMappingURL=json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.js","names":["fs","ops.emptyDiscussion","ops.createThread","ops.addComment","ops.editComment","ops.removeComment","ops.setThreadStatus","ops.updateAnchor","ops.removeThread"],"sources":["../../../src/adapters/comments/json.ts"],"sourcesContent":["import { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { isMissing, writeAtomic } from '../files'\nimport * as ops from './record'\nimport type { Annotation, Comment, CommentStore, CommentThread, Discussion, NewThread, ThreadStatus } from './types'\n\nexport interface JsonCommentStoreOptions {\n /** The one file all documents' discussions live in, e.g. `<data>/concept-discussions.json`; its directory is created on demand. */\n file: string\n}\n\ntype Persisted = Record<string, Discussion>\n\n/** Stores of one process on one file share a queue, so two of them cannot read the same snapshot and lose one write. */\nconst queues = new Map<string, Promise<unknown>>()\n\nfunction serialized<T>(file: string, action: () => Promise<T>): Promise<T> {\n const run = (queues.get(file) ?? Promise.resolve()).then(action)\n const tail = run.catch(() => {})\n queues.set(file, tail)\n void tail.then(() => {\n if (queues.get(file) === tail) queues.delete(file)\n })\n return run\n}\n\nasync function read(file: string): Promise<{ all: Persisted }> {\n let raw: string\n try {\n raw = await fs.readFile(file, 'utf8')\n }\n catch (error) {\n if (isMissing(error)) return { all: {} }\n throw error\n }\n const parsed: unknown = JSON.parse(raw)\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) throw new TypeError(`@ecsplico/editor: ${file} does not hold a JSON object of discussions`)\n return { all: parsed as Persisted }\n}\n\n/** `record` with the two lists every operation relies on (records written by older tools may lack one). */\nfunction complete(record: Discussion | undefined): Discussion | undefined {\n if (!record) return undefined\n record.annotations ??= []\n record.threads ??= []\n return record\n}\n\n/**\n * Comment threads in one JSON file, the layout of skaile-forge's `concept-discussions.json`:\n *\n * { \"<documentId>\": { \"annotations\": [{ id, threadId, anchor }], \"threads\": [{ id, annotationId, status, comments }] } }\n *\n * written with two-space indentation and a trailing newline. Existing files load unchanged and fields this package does\n * not know stay on the records it rewrites; a change that alters nothing does not touch the file. Every write goes\n * through a temp file and a rename and through one queue per file, so concurrent operations (also of two stores on the\n * same file) apply one after another. Single node only: the file is not coordinated across processes.\n */\nexport function createJsonCommentStore(options: JsonCommentStoreOptions): CommentStore {\n const file = path.resolve(options.file)\n\n /** Reads the file, runs `operation` on the document's record and writes the result back when the text changed. `create`: a document without a record gets one. */\n function mutate<T>(documentId: string, create: boolean, operation: (record: Discussion) => T): Promise<T> {\n return serialized(file, async () => {\n const { all } = await read(file)\n const existing = complete(all[documentId])\n const before = JSON.stringify(all)\n const record = existing ?? ops.emptyDiscussion()\n const result = operation(record)\n if (existing || create) all[documentId] = record\n if (JSON.stringify(all) !== before) await writeAtomic(file, `${JSON.stringify(all, null, 2)}\\n`)\n return structuredClone(result)\n })\n }\n\n async function discussion(documentId: string): Promise<Discussion> {\n const { all } = await read(file)\n return structuredClone(complete(all[documentId]) ?? ops.emptyDiscussion())\n }\n\n return {\n getDiscussion: discussion,\n async getThread(documentId, threadId) {\n return (await discussion(documentId)).threads.find(thread => thread.id === threadId)\n },\n async listThreads(documentId) {\n return (await discussion(documentId)).threads\n },\n createThread: (documentId: string, input: NewThread) => mutate(documentId, true, record => ops.createThread(record, input)),\n addComment: (documentId: string, threadId: string, comment: Comment): Promise<CommentThread> => mutate(documentId, false, record => ops.addComment(record, threadId, comment)),\n editComment: (documentId, threadId, commentId, body, editedAt) => mutate(documentId, false, record => ops.editComment(record, threadId, commentId, body, editedAt)),\n removeComment: (documentId, threadId, commentId) => mutate(documentId, false, record => ops.removeComment(record, threadId, commentId)),\n setThreadStatus: (documentId: string, threadId: string, status: ThreadStatus) => mutate(documentId, false, record => ops.setThreadStatus(record, threadId, status)),\n updateAnchor: (documentId: string, annotation: Annotation) => mutate(documentId, false, record => ops.updateAnchor(record, annotation)),\n removeThread: (documentId, threadId) => mutate(documentId, false, record => ops.removeThread(record, threadId)),\n removeDocuments: prefix => serialized(file, async () => {\n const { all } = await read(file)\n const kept: Persisted = {}\n let removed = 0\n for (const [documentId, record] of Object.entries(all)) {\n if (documentId.startsWith(prefix)) removed++\n else kept[documentId] = record\n }\n if (removed) await writeAtomic(file, `${JSON.stringify(kept, null, 2)}\\n`)\n return removed\n }),\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ import { conflict, invalid, notFound } from "./errors.js";
2
+ //#region src/adapters/comments/record.ts
3
+ const emptyDiscussion = () => ({
4
+ annotations: [],
5
+ threads: []
6
+ });
7
+ function requireThread(record, id) {
8
+ const thread = record.threads.find((item) => item.id === id);
9
+ if (!thread) throw notFound("comment thread", id);
10
+ return thread;
11
+ }
12
+ /** Checks the parts of a new thread that do not depend on stored data. */
13
+ function assertNewThread({ annotation, thread }) {
14
+ if (!annotation.id || !thread.id) throw invalid("Annotation and thread need an id.");
15
+ if (thread.annotationId !== annotation.id || annotation.threadId !== thread.id) throw invalid("The annotation and the thread must point to each other.");
16
+ }
17
+ function createThread(record, input) {
18
+ assertNewThread(input);
19
+ if (record.annotations.some((item) => item.id === input.annotation.id)) throw conflict(`Annotation "${input.annotation.id}" already exists.`);
20
+ if (record.threads.some((item) => item.id === input.thread.id)) throw conflict(`Comment thread "${input.thread.id}" already exists.`);
21
+ const annotation = structuredClone(input.annotation);
22
+ const thread = {
23
+ id: input.thread.id,
24
+ annotationId: input.thread.annotationId,
25
+ status: "open",
26
+ comments: [structuredClone(input.thread.initialComment)]
27
+ };
28
+ record.annotations.push(annotation);
29
+ record.threads.push(thread);
30
+ return {
31
+ annotation,
32
+ thread
33
+ };
34
+ }
35
+ function addComment(record, threadId, comment) {
36
+ const thread = requireThread(record, threadId);
37
+ if (thread.comments.some((existing) => existing.id === comment.id)) throw conflict(`Comment "${comment.id}" already exists in thread "${threadId}".`);
38
+ thread.comments.push(structuredClone(comment));
39
+ return thread;
40
+ }
41
+ function editComment(record, threadId, commentId, body, editedAt) {
42
+ const thread = requireThread(record, threadId);
43
+ const comment = thread.comments.find((existing) => existing.id === commentId);
44
+ if (!comment) throw notFound("comment", commentId);
45
+ comment.body = body;
46
+ comment.editedAt = editedAt;
47
+ return thread;
48
+ }
49
+ function removeComment(record, threadId, commentId) {
50
+ const thread = requireThread(record, threadId);
51
+ const index = thread.comments.findIndex((existing) => existing.id === commentId);
52
+ if (index < 0) throw notFound("comment", commentId);
53
+ if (index === 0) throw invalid("The first comment of a thread cannot be removed on its own; remove the thread.");
54
+ thread.comments.splice(index, 1);
55
+ return thread;
56
+ }
57
+ function setThreadStatus(record, threadId, status) {
58
+ const thread = requireThread(record, threadId);
59
+ thread.status = status;
60
+ return thread;
61
+ }
62
+ function updateAnchor(record, annotation) {
63
+ const index = record.annotations.findIndex((item) => item.id === annotation.id);
64
+ const existing = record.annotations[index];
65
+ if (!existing) throw notFound("annotation", annotation.id);
66
+ if (existing.threadId !== annotation.threadId) throw invalid("An annotation cannot move to another thread.");
67
+ const stored = structuredClone(annotation);
68
+ record.annotations[index] = stored;
69
+ return stored;
70
+ }
71
+ function removeThread(record, threadId) {
72
+ record.threads = record.threads.filter((thread) => thread.id !== threadId);
73
+ record.annotations = record.annotations.filter((annotation) => annotation.threadId !== threadId);
74
+ }
75
+ //#endregion
76
+ export { addComment, assertNewThread, createThread, editComment, emptyDiscussion, removeComment, removeThread, requireThread, setThreadStatus, updateAnchor };
77
+
78
+ //# sourceMappingURL=record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.js","names":[],"sources":["../../../src/adapters/comments/record.ts"],"sourcesContent":["import { conflict, invalid, notFound } from './errors'\nimport type { Annotation, Comment, CommentThread, Discussion, NewThread, ThreadStatus } from './types'\n\n// The semantics of every `CommentStore` operation on one document's `Discussion`, for stores that hold the whole\n// record in memory (the JSON file). They mutate `record` in place and throw `CommentStoreError` before changing anything.\n\nexport const emptyDiscussion = (): Discussion => ({ annotations: [], threads: [] })\n\nexport function requireThread(record: Discussion, id: string): CommentThread {\n const thread = record.threads.find(item => item.id === id)\n if (!thread) throw notFound('comment thread', id)\n return thread\n}\n\n/** Checks the parts of a new thread that do not depend on stored data. */\nexport function assertNewThread({ annotation, thread }: NewThread): void {\n if (!annotation.id || !thread.id) throw invalid('Annotation and thread need an id.')\n if (thread.annotationId !== annotation.id || annotation.threadId !== thread.id) throw invalid('The annotation and the thread must point to each other.')\n}\n\nexport function createThread(record: Discussion, input: NewThread): { annotation: Annotation, thread: CommentThread } {\n assertNewThread(input)\n if (record.annotations.some(item => item.id === input.annotation.id)) throw conflict(`Annotation \"${input.annotation.id}\" already exists.`)\n if (record.threads.some(item => item.id === input.thread.id)) throw conflict(`Comment thread \"${input.thread.id}\" already exists.`)\n const annotation = structuredClone(input.annotation)\n const thread: CommentThread = { id: input.thread.id, annotationId: input.thread.annotationId, status: 'open', comments: [structuredClone(input.thread.initialComment)] }\n record.annotations.push(annotation)\n record.threads.push(thread)\n return { annotation, thread }\n}\n\nexport function addComment(record: Discussion, threadId: string, comment: Comment): CommentThread {\n const thread = requireThread(record, threadId)\n if (thread.comments.some(existing => existing.id === comment.id)) throw conflict(`Comment \"${comment.id}\" already exists in thread \"${threadId}\".`)\n thread.comments.push(structuredClone(comment))\n return thread\n}\n\nexport function editComment(record: Discussion, threadId: string, commentId: string, body: string, editedAt: string): CommentThread {\n const thread = requireThread(record, threadId)\n const comment = thread.comments.find(existing => existing.id === commentId)\n if (!comment) throw notFound('comment', commentId)\n comment.body = body\n comment.editedAt = editedAt\n return thread\n}\n\nexport function removeComment(record: Discussion, threadId: string, commentId: string): CommentThread {\n const thread = requireThread(record, threadId)\n const index = thread.comments.findIndex(existing => existing.id === commentId)\n if (index < 0) throw notFound('comment', commentId)\n if (index === 0) throw invalid('The first comment of a thread cannot be removed on its own; remove the thread.')\n thread.comments.splice(index, 1)\n return thread\n}\n\nexport function setThreadStatus(record: Discussion, threadId: string, status: ThreadStatus): CommentThread {\n const thread = requireThread(record, threadId)\n thread.status = status\n return thread\n}\n\nexport function updateAnchor(record: Discussion, annotation: Annotation): Annotation {\n const index = record.annotations.findIndex(item => item.id === annotation.id)\n const existing = record.annotations[index]\n if (!existing) throw notFound('annotation', annotation.id)\n if (existing.threadId !== annotation.threadId) throw invalid('An annotation cannot move to another thread.')\n const stored = structuredClone(annotation)\n record.annotations[index] = stored\n return stored\n}\n\nexport function removeThread(record: Discussion, threadId: string): void {\n record.threads = record.threads.filter(thread => thread.id !== threadId)\n record.annotations = record.annotations.filter(annotation => annotation.threadId !== threadId)\n}\n"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { Annotation, Comment, CommentAction, CommentStore, CommentThread, CreateThreadInput, Discussion, NewThread, ThreadStatus } from "./types.js";
2
+ import { CommentStoreError } from "./errors.js";
3
+ //#region src/adapters/comments/server.d.ts
4
+ export interface CommentsRequest {
5
+ method: string;
6
+ documentId: string;
7
+ /** The parsed JSON body of a `POST`. */
8
+ body?: unknown;
9
+ }
10
+ export interface CommentsResponse {
11
+ status: number;
12
+ /** JSON; `{ error }` for a refused request. */
13
+ body: unknown;
14
+ }
15
+ /**
16
+ * `GET` → `200` with the document's `Discussion`; `POST` with a `CommentAction` → `200` with the changed `thread`/`annotation`
17
+ * (`create`: both; `remove`: `{ ok: true }`). Refused requests answer `404` (unknown ids), `409` (duplicate ids), `400`
18
+ * (malformed input) or `405` with `{ error }`; anything else is a server fault and is thrown.
19
+ */
20
+ export declare function handleCommentsRequest(store: CommentStore, request: CommentsRequest): Promise<CommentsResponse>;
21
+ //#endregion
22
+ export { type Annotation, type Comment, type CommentAction, type CommentStore, CommentStoreError, type CommentThread, type CreateThreadInput, type Discussion, type NewThread, type ThreadStatus };
23
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1,102 @@
1
+ import { isPortableAnchor } from "../anchors.js";
2
+ import { CommentStoreError, invalid } from "./errors.js";
3
+ //#region src/adapters/comments/server.ts
4
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5
+ const isText = (value) => typeof value === "string";
6
+ const isId = (value) => isText(value) && value !== "";
7
+ function commentOf(value) {
8
+ if (!isRecord(value) || !isId(value.id) || !isText(value.body) || !isId(value.authorId) || !isId(value.createdAt)) throw invalid("A comment needs id, body, authorId and createdAt.");
9
+ for (const key of [
10
+ "authorName",
11
+ "authorColor",
12
+ "editedAt"
13
+ ]) if (value[key] !== void 0 && !isText(value[key])) throw invalid(`Comment ${key} must be text.`);
14
+ const { id, body, authorId, createdAt } = value;
15
+ const { authorName, authorColor, editedAt } = value;
16
+ return {
17
+ id,
18
+ body,
19
+ authorId,
20
+ createdAt,
21
+ ...authorName !== void 0 && { authorName },
22
+ ...authorColor !== void 0 && { authorColor },
23
+ ...editedAt !== void 0 && { editedAt }
24
+ };
25
+ }
26
+ function annotationOf(value) {
27
+ if (!isRecord(value) || !isId(value.id) || !isId(value.threadId) || !isPortableAnchor(value.anchor)) throw invalid("An annotation needs id, threadId and a valid anchor.");
28
+ return {
29
+ id: value.id,
30
+ threadId: value.threadId,
31
+ anchor: value.anchor
32
+ };
33
+ }
34
+ function threadInputOf(value) {
35
+ if (!isRecord(value) || !isId(value.id) || !isId(value.annotationId)) throw invalid("A thread needs id, annotationId and initialComment.");
36
+ return {
37
+ id: value.id,
38
+ annotationId: value.annotationId,
39
+ initialComment: commentOf(value.initialComment)
40
+ };
41
+ }
42
+ function idOf(value, key) {
43
+ const id = value[key];
44
+ if (!isId(id)) throw invalid(`"${key}" must be a non-empty string.`);
45
+ return id;
46
+ }
47
+ function statusOf(value) {
48
+ if (value !== "open" && value !== "resolved") throw invalid("\"status\" must be \"open\" or \"resolved\".");
49
+ return value;
50
+ }
51
+ async function perform(store, documentId, body) {
52
+ if (!isRecord(body)) throw invalid("The request body must be a JSON object with an \"action\".");
53
+ switch (body.action) {
54
+ case "create": return store.createThread(documentId, {
55
+ annotation: annotationOf(body.annotation),
56
+ thread: threadInputOf(body.thread)
57
+ });
58
+ case "reply": return { thread: await store.addComment(documentId, idOf(body, "threadId"), commentOf(body.comment)) };
59
+ case "status": return { thread: await store.setThreadStatus(documentId, idOf(body, "threadId"), statusOf(body.status)) };
60
+ case "update-annotation": return { annotation: await store.updateAnchor(documentId, annotationOf(body.annotation)) };
61
+ case "remove":
62
+ await store.removeThread(documentId, idOf(body, "threadId"));
63
+ return { ok: true };
64
+ case "edit":
65
+ if (!isText(body.body)) throw invalid("\"body\" must be text.");
66
+ return { thread: await store.editComment(documentId, idOf(body, "threadId"), idOf(body, "commentId"), body.body, isId(body.editedAt) ? body.editedAt : (/* @__PURE__ */ new Date()).toISOString()) };
67
+ case "remove-comment": return { thread: await store.removeComment(documentId, idOf(body, "threadId"), idOf(body, "commentId")) };
68
+ default: throw invalid("Unknown comment action.");
69
+ }
70
+ }
71
+ /**
72
+ * `GET` → `200` with the document's `Discussion`; `POST` with a `CommentAction` → `200` with the changed `thread`/`annotation`
73
+ * (`create`: both; `remove`: `{ ok: true }`). Refused requests answer `404` (unknown ids), `409` (duplicate ids), `400`
74
+ * (malformed input) or `405` with `{ error }`; anything else is a server fault and is thrown.
75
+ */
76
+ async function handleCommentsRequest(store, request) {
77
+ const method = request.method.toUpperCase();
78
+ try {
79
+ if (method === "GET") return {
80
+ status: 200,
81
+ body: await store.getDiscussion(request.documentId)
82
+ };
83
+ if (method === "POST") return {
84
+ status: 200,
85
+ body: await perform(store, request.documentId, request.body)
86
+ };
87
+ return {
88
+ status: 405,
89
+ body: { error: `Method ${request.method} is not allowed.` }
90
+ };
91
+ } catch (error) {
92
+ if (error instanceof CommentStoreError) return {
93
+ status: error.status,
94
+ body: { error: error.message }
95
+ };
96
+ throw error;
97
+ }
98
+ }
99
+ //#endregion
100
+ export { CommentStoreError, handleCommentsRequest };
101
+
102
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","names":[],"sources":["../../../src/adapters/comments/server.ts"],"sourcesContent":["import { isPortableAnchor } from '../anchors'\nimport { CommentStoreError, invalid } from './errors'\nimport type { Annotation, Comment, CommentStore, CreateThreadInput, ThreadStatus } from './types'\n\n// Framework-free REST handler of a document's comments endpoint: the host routes `GET`/`POST` of\n// `<base>/<documentId>` here after its own authentication and authorisation (the handler trusts the caller, including\n// the `authorId` of new comments), and answers with `status` and `body`. Nitro, Express or a test's fetch stub all fit.\n\nexport type { Annotation, Comment, CommentAction, CommentStore, CommentThread, CreateThreadInput, Discussion, NewThread, ThreadStatus } from './types'\nexport { CommentStoreError } from './errors'\n\nexport interface CommentsRequest {\n method: string\n documentId: string\n /** The parsed JSON body of a `POST`. */\n body?: unknown\n}\n\nexport interface CommentsResponse {\n status: number\n /** JSON; `{ error }` for a refused request. */\n body: unknown\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)\nconst isText = (value: unknown): value is string => typeof value === 'string'\nconst isId = (value: unknown): value is string => isText(value) && value !== ''\n\nfunction commentOf(value: unknown): Comment {\n if (!isRecord(value) || !isId(value.id) || !isText(value.body) || !isId(value.authorId) || !isId(value.createdAt)) throw invalid('A comment needs id, body, authorId and createdAt.')\n for (const key of ['authorName', 'authorColor', 'editedAt']) {\n if (value[key] !== undefined && !isText(value[key])) throw invalid(`Comment ${key} must be text.`)\n }\n const { id, body, authorId, createdAt } = value\n const { authorName, authorColor, editedAt } = value as Record<string, string | undefined>\n return { id, body, authorId, createdAt, ...(authorName !== undefined && { authorName }), ...(authorColor !== undefined && { authorColor }), ...(editedAt !== undefined && { editedAt }) }\n}\n\nfunction annotationOf(value: unknown): Annotation {\n if (!isRecord(value) || !isId(value.id) || !isId(value.threadId) || !isPortableAnchor(value.anchor)) throw invalid('An annotation needs id, threadId and a valid anchor.')\n return { id: value.id, threadId: value.threadId, anchor: value.anchor }\n}\n\nfunction threadInputOf(value: unknown): CreateThreadInput {\n if (!isRecord(value) || !isId(value.id) || !isId(value.annotationId)) throw invalid('A thread needs id, annotationId and initialComment.')\n return { id: value.id, annotationId: value.annotationId, initialComment: commentOf(value.initialComment) }\n}\n\nfunction idOf(value: Record<string, unknown>, key: string): string {\n const id = value[key]\n if (!isId(id)) throw invalid(`\"${key}\" must be a non-empty string.`)\n return id\n}\n\nfunction statusOf(value: unknown): ThreadStatus {\n if (value !== 'open' && value !== 'resolved') throw invalid('\"status\" must be \"open\" or \"resolved\".')\n return value\n}\n\nasync function perform(store: CommentStore, documentId: string, body: unknown): Promise<unknown> {\n if (!isRecord(body)) throw invalid('The request body must be a JSON object with an \"action\".')\n switch (body.action) {\n case 'create':\n return store.createThread(documentId, { annotation: annotationOf(body.annotation), thread: threadInputOf(body.thread) })\n case 'reply':\n return { thread: await store.addComment(documentId, idOf(body, 'threadId'), commentOf(body.comment)) }\n case 'status':\n return { thread: await store.setThreadStatus(documentId, idOf(body, 'threadId'), statusOf(body.status)) }\n case 'update-annotation':\n return { annotation: await store.updateAnchor(documentId, annotationOf(body.annotation)) }\n case 'remove':\n await store.removeThread(documentId, idOf(body, 'threadId'))\n return { ok: true }\n case 'edit':\n if (!isText(body.body)) throw invalid('\"body\" must be text.')\n return { thread: await store.editComment(documentId, idOf(body, 'threadId'), idOf(body, 'commentId'), body.body, isId(body.editedAt) ? body.editedAt : new Date().toISOString()) }\n case 'remove-comment':\n return { thread: await store.removeComment(documentId, idOf(body, 'threadId'), idOf(body, 'commentId')) }\n default:\n throw invalid('Unknown comment action.')\n }\n}\n\n/**\n * `GET` → `200` with the document's `Discussion`; `POST` with a `CommentAction` → `200` with the changed `thread`/`annotation`\n * (`create`: both; `remove`: `{ ok: true }`). Refused requests answer `404` (unknown ids), `409` (duplicate ids), `400`\n * (malformed input) or `405` with `{ error }`; anything else is a server fault and is thrown.\n */\nexport async function handleCommentsRequest(store: CommentStore, request: CommentsRequest): Promise<CommentsResponse> {\n const method = request.method.toUpperCase()\n try {\n if (method === 'GET') return { status: 200, body: await store.getDiscussion(request.documentId) }\n if (method === 'POST') return { status: 200, body: await perform(store, request.documentId, request.body) }\n return { status: 405, body: { error: `Method ${request.method} is not allowed.` } } // i18n-ignore: HTTP error body of the comments route (server messages are not localised, like src/server)\n }\n catch (error) {\n if (error instanceof CommentStoreError) return { status: error.status, body: { error: error.message } }\n throw error\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ import { PortableAnchor } from "../anchors.js";
2
+ //#region src/adapters/comments/types.d.ts
3
+ export type ThreadStatus = 'open' | 'resolved';
4
+ /** One message of a thread; the first is the thread's own text, the rest are replies. */
5
+ export interface Comment {
6
+ id: string;
7
+ body: string;
8
+ authorId: string;
9
+ /** ISO timestamp. */
10
+ createdAt: string;
11
+ /** Display name and colour of the author when the writer knew them; older data has only `authorId`. */
12
+ authorName?: string;
13
+ authorColor?: string;
14
+ /** ISO timestamp of the last change of `body`. */
15
+ editedAt?: string;
16
+ }
17
+ /** A discussion: one annotation's messages, `open` or `resolved`. */
18
+ export interface CommentThread {
19
+ id: string;
20
+ annotationId: string;
21
+ status: ThreadStatus;
22
+ comments: Comment[];
23
+ }
24
+ /** Where a thread points to in the document. */
25
+ export interface Annotation {
26
+ id: string;
27
+ threadId: string;
28
+ anchor: PortableAnchor;
29
+ }
30
+ /** Everything stored for one document. */
31
+ export interface Discussion {
32
+ annotations: Annotation[];
33
+ threads: CommentThread[];
34
+ }
35
+ export interface CreateThreadInput {
36
+ id: string;
37
+ annotationId: string;
38
+ initialComment: Comment;
39
+ }
40
+ /** A new thread together with the annotation it hangs on; stored as one unit. */
41
+ export interface NewThread {
42
+ annotation: Annotation;
43
+ thread: CreateThreadInput;
44
+ }
45
+ /**
46
+ * Where a host keeps the comment threads of its documents, keyed by document id. Persistence only: who may read or
47
+ * write, and who the author is, stay with the host (`handleCommentsRequest` takes already-authorised calls). Every
48
+ * method rejects with a `CommentStoreError` for caller mistakes (unknown or duplicate ids, inconsistent input) and
49
+ * never partially applies a change.
50
+ */
51
+ export interface CommentStore {
52
+ /** The annotations and threads of `documentId`, in the order they were created; empty for an unknown document. */
53
+ getDiscussion(documentId: string): Promise<Discussion>;
54
+ getThread(documentId: string, threadId: string): Promise<CommentThread | undefined>;
55
+ listThreads(documentId: string): Promise<CommentThread[]>;
56
+ /** Stores the annotation and the thread with its first comment. Duplicate annotation or thread ids are a conflict. */
57
+ createThread(documentId: string, input: NewThread): Promise<{
58
+ annotation: Annotation;
59
+ thread: CommentThread;
60
+ }>;
61
+ /** Appends a reply. */
62
+ addComment(documentId: string, threadId: string, comment: Comment): Promise<CommentThread>;
63
+ /** Changes the text of one comment (the first one is the thread's own text) and stamps it `editedAt`. */
64
+ editComment(documentId: string, threadId: string, commentId: string, body: string, editedAt: string): Promise<CommentThread>;
65
+ /** Deletes one reply. The first comment cannot be deleted on its own: remove the thread. */
66
+ removeComment(documentId: string, threadId: string, commentId: string): Promise<CommentThread>;
67
+ setThreadStatus(documentId: string, threadId: string, status: ThreadStatus): Promise<CommentThread>;
68
+ /** Replaces the anchor of an existing annotation (after the text moved, or became an orphan). */
69
+ updateAnchor(documentId: string, annotation: Annotation): Promise<Annotation>;
70
+ /** Deletes the thread and the annotations that point to it; unknown ids are fine. */
71
+ removeThread(documentId: string, threadId: string): Promise<void>;
72
+ /** Deletes the discussions of every document whose id starts with `prefix` (a deleted workspace's `<id>/`); the number of documents that had any. */
73
+ removeDocuments(prefix: string): Promise<number>;
74
+ }
75
+ /**
76
+ * The body of a `POST` to a document's comments endpoint (`handleCommentsRequest` answers it, `createRestCommentsModel`
77
+ * sends it). `GET` answers the document's `Discussion`. The first five actions are skaile-forge's original wire format.
78
+ */
79
+ export type CommentAction = {
80
+ action: 'create';
81
+ annotation: Annotation;
82
+ thread: CreateThreadInput;
83
+ } | {
84
+ action: 'reply';
85
+ threadId: string;
86
+ comment: Comment;
87
+ } | {
88
+ action: 'status';
89
+ threadId: string;
90
+ status: ThreadStatus;
91
+ } | {
92
+ action: 'update-annotation';
93
+ annotation: Annotation;
94
+ } | {
95
+ action: 'remove';
96
+ threadId: string;
97
+ } | {
98
+ action: 'edit';
99
+ threadId: string;
100
+ commentId: string;
101
+ body: string;
102
+ editedAt?: string;
103
+ } | {
104
+ action: 'remove-comment';
105
+ threadId: string;
106
+ commentId: string;
107
+ };
108
+ //#endregion
109
+ //# sourceMappingURL=types.d.ts.map