@ecsplico/editor 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +1 -1
- package/dist/adapters/comments/client.js +2 -2
- package/dist/adapters/comments/client.js.map +1 -1
- package/dist/adapters/comments/drizzle.js +9 -9
- package/dist/adapters/comments/drizzle.js.map +1 -1
- package/dist/adapters/comments/errors.js +1 -1
- package/dist/adapters/comments/errors.js.map +1 -1
- package/dist/adapters/comments/record.js +11 -11
- package/dist/adapters/comments/record.js.map +1 -1
- package/dist/adapters/comments/server.js +10 -10
- package/dist/adapters/comments/server.js.map +1 -1
- package/dist/adapters/vue.js +2 -2
- package/dist/adapters/vue.js.map +1 -1
- package/dist/features/block-ui/block-actions.d.ts +21 -0
- package/dist/features/block-ui/block-actions.js +25 -1
- package/dist/features/block-ui/block-actions.js.map +1 -1
- package/dist/features/block-ui/components/BlockActionsOverlay.js +33 -4
- package/dist/features/block-ui/components/BlockActionsOverlay.js.map +1 -1
- package/dist/features/block-ui/components/BlockActionsOverlay.vue.d.ts +4 -0
- package/dist/features/block-ui/index.d.ts +2 -2
- package/dist/features/block-ui/index.js +2 -2
- package/dist/features/block-ui/react/BlockActionsOverlay.d.ts +4 -0
- package/dist/features/block-ui/react/BlockActionsOverlay.js +38 -12
- package/dist/features/block-ui/react/BlockActionsOverlay.js.map +1 -1
- package/dist/features/comments/model.js.map +1 -1
- package/dist/markdown/MarkdownSurface.vue.d.ts +1 -1
- package/dist/tree/tree-host.js +1 -1
- package/dist/tree/tree-items.js +1 -1
- package/docs/integration.md +3 -1
- package/docs/llms.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.2
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `BLOCK_CORNER_ATTR` (`data-block-corner`), `blockCornerOf(dom)` and the type `BlockCorner` from `/block-ui`: a block marks the icons in its top-right corner with the attribute, and the hover action bar (Vue and React `BlockActionsOverlay`) docks immediately left of them, vertically centred, instead of covering them. Blocks without marked elements keep the bar at the editor's right edge.
|
|
8
|
+
|
|
9
|
+
## 2.1.1
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The comment stores and `handleCommentsRequest` (`/comments/server`, `/comments/json`, `/comments/drizzle`) answer refused requests (`400`/`404`/`405`/`409`) with German `error` texts, like the editor's other server routes (e.g. `Kommentar-Thread „t1“ nicht gefunden`). Status codes and `CommentStoreError.code` are unchanged; clients that matched the old English texts must match the status or code instead.
|
|
14
|
+
- Errors thrown by `useEditorDocument` (`/adapters/vue`) and `createRestCommentsModel` (`/comments/client`) start with `@ecsplico/editor:` like the editor's other developer errors.
|
|
15
|
+
|
|
3
16
|
## 2.1.0
|
|
4
17
|
|
|
5
18
|
### Added
|
|
@@ -8,6 +21,18 @@
|
|
|
8
21
|
- Optional peer dependency `drizzle-orm` (`^0.45.0`), needed only by `/adapters/drizzle` and `/comments/drizzle`.
|
|
9
22
|
- Bundle-isolation fixtures `adapters-node`, `adapters-drizzle`, `adapters-nitro`, `adapters-vue` and `headless-comments-client`: Vue is reachable only from `/adapters/vue`, Nitro only from `/server/nitro`, drizzle only from the two drizzle entries, React from none.
|
|
10
23
|
- Example `examples/vue-nitro-json` (`bun run proto:vue-nitro-json`): a Vue editor on a bare Nitro server with JSON-file storage.
|
|
24
|
+
- Framework-free models behind the Vue and React components: `createCompositeInspector` (`/composites`), `createAiSelectionFlow` (`/ai`) and the comments margin (`createMarginController`, `layoutMargin`, `marginCards` in `/comments`; orphaned threads sit at their deletion point). The document tree reports why a drop is refused and filters by title.
|
|
25
|
+
- Accepted children per Baustein kind, enforced in the editor: `AcceptedChildren`, `kindAccepts`, `moveIntoVerdict` (`/composites`).
|
|
26
|
+
- The block-lines module owning the `[id] …` line format the AI sees (`readBlockLines`, `BlockLines` and the `BLOCK_LINE_*` texts in `/ai` and `/ai/protocol`): IDs inside fenced code are ignored, `add` requires `afterId`, operations on a block inside a block another operation replaces or deletes are dropped (`AgentOpProblemReason` `inside`), and operations on blocks deleted since the document went to the model are reported as `stale` (`AgentOpOutcome`).
|
|
27
|
+
|
|
28
|
+
### Changed (breaking)
|
|
29
|
+
|
|
30
|
+
These shipped in a minor release by mistake; the only known consumer (skaile-forge) is migrated.
|
|
31
|
+
|
|
32
|
+
- `documentBlockIds(text)` is removed from `/ai` and `/ai/protocol`: use `readBlockLines(text).ids`.
|
|
33
|
+
- `AgentOpContext` (`checkAgentOps`): `known: (id) => boolean` is replaced by `lines: BlockLines`; pass `lines: readBlockLines(documentChatText(editor))`.
|
|
34
|
+
- `CommentsSnapshot` has a required `orphanAt` (thread id → deletion position); a host's own `CommentsModel` must fill it (an empty map is valid).
|
|
35
|
+
- `AgentOpOutcome` and `AgentOpProblemReason` gained `stale` and `inside`; exhaustive maps over them need the new keys.
|
|
11
36
|
|
|
12
37
|
### Notes
|
|
13
38
|
|
package/README.md
CHANGED
|
@@ -132,7 +132,7 @@ Every entry is an explicit subpath of `package.json#exports`; there is no wildca
|
|
|
132
132
|
| `@ecsplico/editor/integrations/composite-media` | Framework-free: `compositeMediaFeature` (requires `composites`, `media`): imported files become `document` composites |
|
|
133
133
|
| `@ecsplico/editor/integrations/transcript-ai`, `/transcript-ai/protocol` | Framework-free: `transcriptAiFeature` (requires `transcript`, `ai`), `createTranscriptCleanup`, `hasTranscriptSegments`; server contract: `TRANSCRIPT_CLEANUP_LIMITS`, `transcriptCleanupRequestSchema`, `transcriptCleanupMessages`, `transcriptCleanupChunks`, `parseTranscriptCleanup` |
|
|
134
134
|
| `@ecsplico/editor/collab`, `/collab/vue`, `/collab/react` | Framework-free: `collabFeatureCore(ui?)` (Yjs `Collaboration`/`CollaborationCaret`, active only with a `collaboration` session on the surface), `yjsAnchorResolver`; `/vue` and `/react`: `collabFeature()` (the Vue one adds the `@tiptap/vue-3` state-sync workaround, the React one needs none) |
|
|
135
|
-
| `@ecsplico/editor/block-ui`, `/block-ui/vue`, `/block-ui/react` | `/vue` and `/react`: `blockUiFeature()`, `BlockUi`, `BlockActionsOverlay`; framework-free: `blockUiFeatureCore`, `blockActionsFor`, `effectiveTarget
|
|
135
|
+
| `@ecsplico/editor/block-ui`, `/block-ui/vue`, `/block-ui/react` | `/vue` and `/react`: `blockUiFeature()`, `BlockUi`, `BlockActionsOverlay`; framework-free: `blockUiFeatureCore`, `blockActionsFor`, `effectiveTarget`, `BLOCK_CORNER_ATTR` and `blockCornerOf` (a node view marks its own top-right icons with `data-block-corner`; the hover bar docks left of them): drag handle, hover targeting and block actions (opt-in; the simple editor never includes it) |
|
|
136
136
|
| `@ecsplico/editor/schema/advanced` | Vue-free `advancedSchemaExtensions()`: the advanced schema for servers, validators and migrations |
|
|
137
137
|
| `@ecsplico/editor/server`, `/server/handlers`, `/server/node`, `/nuxt` | Server side without a web framework: `createCollabServer`, `createAiModels`, `createRateLimiter`, `EditorAdapter`, one request handler per route (`/server/handlers`), `listenCollab` for a plain Node script (`/server/node`); the Nitro registry `setEditorAdapter`, `forgetCollabDocument`, `liveContent`, `collabStats`; the Nuxt module (config key `tiptapEditor`) wraps the same core. See the [integration guide](./docs/integration.md) |
|
|
138
138
|
| `@ecsplico/editor/styles/core.css` (required), `/composites.css`, `/media.css`, `/mentions.css`, `/tables.css`, `/authors.css`, `/comments.css`, `/transcript.css`, `/ai.css`, `/collab.css`, `/block-ui.css`, `/themes.css` | Feature CSS (import after Tailwind, Nuxt UI (Vue) or `ui-vars.css` (React) and `core.css`; `themes.css` after the feature sheets). The Nuxt module adds all of them itself |
|
|
@@ -138,7 +138,7 @@ function createRestCommentsModel(options) {
|
|
|
138
138
|
});
|
|
139
139
|
if (!response.ok) {
|
|
140
140
|
const detail = await response.json().then(errorOf, () => void 0);
|
|
141
|
-
throw new Error(
|
|
141
|
+
throw new Error(`@ecsplico/editor: comments request ${init?.method ?? "GET"} ${endpoint} failed: ${response.status}${typeof detail === "string" ? ` ${detail}` : ""}`);
|
|
142
142
|
}
|
|
143
143
|
return response.json();
|
|
144
144
|
}
|
|
@@ -328,7 +328,7 @@ function createRestCommentsModel(options) {
|
|
|
328
328
|
}
|
|
329
329
|
async function load() {
|
|
330
330
|
const discussion = await request();
|
|
331
|
-
if (!isDiscussion(discussion)) throw new Error(
|
|
331
|
+
if (!isDiscussion(discussion)) throw new Error(`@ecsplico/editor: comments request GET ${endpoint} did not answer a discussion`);
|
|
332
332
|
threads = discussion.threads;
|
|
333
333
|
anchors = new Map(discussion.annotations.map((annotation) => [annotation.id, annotation.anchor]));
|
|
334
334
|
persisted.clear();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["EMPTY"],"sources":["../../../src/adapters/comments/client.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport type { Transaction } from '@tiptap/pm/state'\nimport { type AuthorIdentity, createStore, releaseRanges, resolveRange } from '../../core'\nimport { cancelCommentDraft, COMMENT_LIMITS, type CommentReply, type CommentsModel, type CommentsSnapshot, type CommentThread as EditorThread, commentsKey, type DocRange, setActiveComment, startCommentDraft } from '../../features/comments'\nimport { EMPTY_SNAPSHOT as EMPTY, NO_POSITIONS, NO_RANGES, NO_THREADS, sameOrphanAt, sameRange, sameRanges, sameUser } from '../../features/comments/snapshot-equality'\nimport { captureAnchor, capturePoint, type PortableAnchor, resolveAnchor, mapAnchor } from '../anchors'\nimport type { Annotation, Comment, CommentAction, CommentThread, Discussion } from './types'\n\n// A `CommentsModel` for the editor's comments feature (`commentsFeature({ model })`) whose threads live behind a REST\n// endpoint (`handleCommentsRequest` serves it) instead of the document's Y.Doc. The text a thread hangs on is a\n// portable anchor (`../anchors`), not a `comment` mark: anchors are resolved against the document when the threads\n// load, follow every edit of the editor, and are written back to the endpoint when they moved.\n\n/** The part of a `Response` the model reads; `globalThis.fetch` fits. */\nexport interface CommentsFetchResponse {\n ok: boolean\n status: number\n json(): Promise<unknown>\n}\n\nexport type CommentsFetch = (url: string, init?: { method?: string, headers?: Record<string, string>, body?: string }) => Promise<CommentsFetchResponse>\n\nexport interface RestCommentsModelOptions {\n /** How requests are sent: `globalThis.fetch`, or a wrapper that adds the base URL and credentials. */\n fetch: CommentsFetch\n /** The document the threads belong to. */\n documentId: string\n /** The document's comments endpoint (`GET` → `Discussion`, `POST` → `CommentAction`); default `/comments/<encoded documentId>`. */\n endpoint?: string\n /** Who writes: the author of new comments and the only one who may edit or delete them; `null` while nobody is signed in (no comments then). */\n user: AuthorIdentity | null | (() => AuthorIdentity | null)\n /** Whether the user may compose and change comments now; readers of a read-only document may be allowed. */\n canComment: boolean | (() => boolean)\n /** The editor the threads belong to; may return no editor yet or a destroyed one. */\n getEditor: () => Editor | undefined\n /** Milliseconds a changed anchor waits before it is written back (typing moves many); default 500. `idle()` writes at once. */\n persistDelay?: number\n /** Called with every failed request; the model reloads the threads afterwards, so the local state falls back to the server's. */\n onError?: (error: Error) => void\n}\n\nexport interface RestCommentsModel extends CommentsModel {\n /** Fetches the threads again and resolves their anchors against the current document. */\n reload(): Promise<void>\n /** Resolves once every change made so far has been sent (and the threads loaded, if a load was requested). */\n idle(): Promise<void>\n}\n\n/** Neutral colour of authors that stored no colour. */\nconst FALLBACK_COLOR = '#6b7280'\n/** Characters of context that must agree for an orphan to be re-attached on load. */\nconst REATTACH_CONTEXT = 24\n\nconst newId = (prefix: string) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`\n/** The `error` message of a refused request's JSON body. */\nfunction errorOf(body: unknown): unknown {\n return typeof body === 'object' && body !== null && 'error' in body ? body.error : undefined\n}\n\nfunction isDiscussion(value: unknown): value is Discussion {\n return typeof value === 'object' && value !== null && 'annotations' in value && Array.isArray(value.annotations) && 'threads' in value && Array.isArray(value.threads)\n}\n\nconst sameJson = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b)\n\nconst authorOf = (comment: Comment): EditorThread['author'] => ({ id: comment.authorId, name: comment.authorName ?? comment.authorId, color: comment.authorColor ?? FALLBACK_COLOR })\n\nfunction quoteOf(anchor: PortableAnchor | undefined): string {\n if (!anchor) return ''\n if (anchor.kind === 'range') return anchor.quote\n if (anchor.kind === 'orphan') return anchor.quote ?? (anchor.previous.kind === 'range' ? anchor.previous.quote : '')\n return ''\n}\n\nfunction viewOf(thread: CommentThread, anchor: PortableAnchor | undefined): EditorThread {\n const [first, ...replies] = thread.comments\n return {\n id: thread.id,\n author: first ? authorOf(first) : { id: '', name: '', color: FALLBACK_COLOR },\n text: first?.body ?? '',\n createdAt: first?.createdAt ?? '',\n ...(first?.editedAt !== undefined && { editedAt: first.editedAt }),\n quote: quoteOf(anchor),\n resolved: thread.status === 'resolved',\n replies: replies.map((reply): CommentReply => ({\n id: reply.id,\n author: authorOf(reply),\n text: reply.body,\n createdAt: reply.createdAt,\n ...(reply.editedAt !== undefined && { editedAt: reply.editedAt }),\n })),\n }\n}\n\nfunction rangeOf(anchor: PortableAnchor | undefined): DocRange | null {\n if (!anchor || anchor.kind === 'orphan') return null\n return anchor.kind === 'range' ? { from: anchor.from, to: anchor.to } : { from: anchor.position, to: anchor.position }\n}\n\n/** A document is \"empty\" while the host has not put its content in yet: nothing to resolve anchors against. */\nconst isBlank = (doc: { textContent: string }) => doc.textContent === ''\n\n/** Whether the text at `candidate` still reads like the context `previous` was written in (one side suffices). */\nfunction contextAgrees(candidate: PortableAnchor, previous: PortableAnchor): boolean {\n if (candidate.kind === 'orphan' || previous.kind === 'orphan') return false\n const before = previous.contextBefore?.slice(-REATTACH_CONTEXT)\n const after = previous.contextAfter?.slice(0, REATTACH_CONTEXT)\n if (!before && !after) return true\n return (!!before && !!candidate.contextBefore?.endsWith(before)) || (!!after && !!candidate.contextAfter?.startsWith(after))\n}\n\n/**\n * The comments of one editor over a REST endpoint, as a `CommentsModel` (the host passes `() => model` as the\n * `model` factory of `commentsFeature`/`commentsFeatureCore`; one instance serves the plugin and every hook).\n *\n * - **Load.** `refresh()` (called by the feature once the editor exists) fetches `GET endpoint`; each annotation's\n * anchor is resolved against the document (exact offsets, else quote and context search). An anchor that moved is\n * re-captured and written back; an anchor whose text is gone becomes an `orphan` (the thread stays, without a range)\n * and an orphan whose text is back with the same surroundings is re-attached. While the document is still empty the\n * resolving waits for its content.\n * - **Edits.** Every transaction carries all anchors with it (`mapAnchor`), so ranges, quotes and contexts follow the\n * text; changed anchors are written back after `persistDelay`. Text deleted under an anchor orphans it for good (it is\n * never silently re-attached to other text during editing; `reload()` re-attaches it when its text and context\n * are found again, e.g. after the content was replaced).\n * - **Writes** are applied locally at once and sent in order; a failed request is reported to `onError` and the threads\n * are reloaded. Without a `user` or while `canComment` is false nothing is written. Only the author may edit or\n * delete a comment.\n * - **Lifecycle.** The model follows the editor `getEditor` names until that editor is destroyed; `dispose()` writes\n * pending anchor changes and does not stop it (the feature and its hooks share the instance and dispose it on their\n * own schedule).\n */\nexport function createRestCommentsModel(options: RestCommentsModelOptions): RestCommentsModel {\n const { fetch, documentId, getEditor, onError } = options\n const endpoint = options.endpoint ?? `/comments/${encodeURIComponent(documentId)}`\n const persistDelay = options.persistDelay ?? 500\n const store = createStore<CommentsSnapshot>(EMPTY)\n\n const userNow = () => (typeof options.user === 'function' ? options.user() : options.user)\n const canCommentNow = () => (typeof options.canComment === 'function' ? options.canComment() : options.canComment)\n /** Editors that announced their destruction: their view is still alive while `destroy` is emitted. */\n const gone = new WeakSet<Editor>()\n const current = () => {\n const instance = getEditor()\n return instance && !instance.isDestroyed && !gone.has(instance) ? instance : undefined\n }\n\n // -- Data: what the endpoint holds (annotation anchors are the local truth until written back) -----------------------\n let threads: readonly CommentThread[] = []\n let anchors = new Map<string, PortableAnchor>()\n /** The anchor each annotation has on the server, serialised. */\n const persisted = new Map<string, string>()\n const dirty = new Set<string>()\n /**\n * Where the text of an annotation that became an orphan during this session was: the deletion point, mapped through\n * every later edit. Keyed by annotation id (`orphanAt` in the snapshot is keyed by thread id; `orphanPositions` translates).\n * Deletion points are session-local: `load()` clears them, and an anchor that arrives already orphaned has none, because\n * the server stores no deletion point. Such threads get no `orphanAt` entry and sit after the placed ones in the margin.\n */\n const deletedAt = new Map<string, number>()\n /** Anchors wait for the document content (resolve on load) before they get a range. */\n let unresolved = false\n /** A load was requested / the first one succeeded: writes wait for it, so a load never overwrites a newer local change. */\n let loaded = false\n let ready = false\n\n // -- Requests: one queue, so a reply never overtakes its thread's creation ---------------------------------------------\n let chain: Promise<void> = Promise.resolve()\n let timer: ReturnType<typeof setTimeout> | undefined\n\n function enqueue(task: () => Promise<void>, reloadOnError = true): void {\n chain = chain.then(task).catch((error: unknown) => {\n onError?.(error instanceof Error ? error : new Error(String(error)))\n if (reloadOnError) enqueue(load, false)\n })\n }\n\n async function request(init?: { method: string, body: CommentAction }): Promise<unknown> {\n const response = await fetch(endpoint, init && { method: init.method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(init.body) })\n if (!response.ok) {\n const detail = await response.json().then(errorOf, () => undefined)\n throw new Error(`Comments request ${init?.method ?? 'GET'} ${endpoint} failed: ${response.status}${typeof detail === 'string' ? ` ${detail}` : ''}`)\n }\n return response.json()\n }\n\n const send = (body: CommentAction) => enqueue(async () => void await request({ method: 'POST', body }))\n\n function writeAnchors(): void {\n timer = undefined\n if (!userNow() || !ready || !canCommentNow()) {\n dirty.clear()\n return\n }\n for (const id of dirty) {\n const anchor = anchors.get(id)\n const threadId = threads.find(thread => thread.annotationId === id)?.id\n if (!anchor || !threadId || persisted.get(id) === JSON.stringify(anchor)) continue\n persisted.set(id, JSON.stringify(anchor))\n send({ action: 'update-annotation', annotation: { id, threadId, anchor } })\n }\n dirty.clear()\n }\n\n function markDirty(id: string): void {\n dirty.add(id)\n if (timer === undefined) timer = setTimeout(writeAnchors, persistDelay)\n }\n\n function flush(): void {\n clearTimeout(timer)\n writeAnchors()\n }\n\n // -- Anchors ------------------------------------------------------------------------------------------------------------\n\n /** `anchor` read against `editor`'s document: the moved/re-captured anchor, an orphan, or the same object. */\n function resolveOne(editor: Editor, anchor: PortableAnchor): PortableAnchor {\n const live = anchor.kind === 'orphan' ? anchor.previous : anchor\n const found = resolveAnchor(editor.state.doc, live)\n if (!found) {\n return anchor.kind === 'orphan'\n ? anchor\n : { kind: 'orphan', previous: anchor, quote: quoteOf(anchor) || undefined, contextBefore: anchor.contextBefore, contextAfter: anchor.contextAfter, reason: 'unresolvable' }\n }\n const next = live.kind === 'range' ? captureAnchor(editor, found) : capturePoint(editor, found.from)\n if (anchor.kind === 'orphan' && !contextAgrees(next, live)) return anchor\n return sameJson(next, anchor) ? anchor : next\n }\n\n function resolveAll(editor: Editor): void {\n if (isBlank(editor.state.doc)) {\n unresolved = anchors.size > 0\n return\n }\n unresolved = false\n for (const [id, anchor] of anchors) {\n const next = resolveOne(editor, anchor)\n if (next !== anchor) {\n anchors.set(id, next)\n if (next.kind !== 'orphan') deletedAt.delete(id)\n markDirty(id)\n }\n }\n }\n\n function carry(transactions: readonly Transaction[]): void {\n for (const transaction of transactions) {\n if (!transaction.docChanged) continue\n for (const [id, position] of deletedAt) deletedAt.set(id, transaction.mapping.map(position, 1))\n for (const [id, anchor] of anchors) {\n const next = mapAnchor(anchor, transaction)\n if (next !== anchor) {\n anchors.set(id, next)\n if (next.kind === 'orphan' && anchor.kind !== 'orphan') {\n deletedAt.set(id, transaction.mapping.map(anchor.kind === 'range' ? anchor.from : anchor.position, 1))\n }\n markDirty(id)\n }\n }\n }\n }\n\n // -- Snapshot -----------------------------------------------------------------------------------------------------------\n\n function rangesOf(editor: Editor | undefined): ReadonlyMap<string, DocRange> {\n if (!editor || unresolved) return NO_RANGES\n const size = editor.state.doc.content.size\n const ranges = new Map<string, DocRange>()\n for (const thread of threads) {\n const range = rangeOf(anchors.get(thread.annotationId))\n if (range && range.from <= size) ranges.set(thread.id, { from: range.from, to: Math.min(range.to, size) })\n }\n return ranges\n }\n\n /**\n * The snapshot's `orphanAt`, keyed by thread id: where each open thread without a text range was, read from `deletedAt`\n * (keyed by annotation id). Empty when there is no tracked deletion point; threads loaded already orphaned never have one\n * (the server stores none) and are left out, so they sit after the placed ones in the margin.\n */\n function orphanPositions(editor: Editor | undefined, ranges: ReadonlyMap<string, DocRange>): ReadonlyMap<string, number> {\n if (!editor || unresolved || deletedAt.size === 0) return NO_POSITIONS\n const size = editor.state.doc.content.size\n let found: Map<string, number> | undefined\n for (const thread of threads) {\n if (thread.status === 'resolved' || ranges.has(thread.id)) continue\n const position = deletedAt.get(thread.annotationId)\n if (position !== undefined && position <= size) (found ??= new Map()).set(thread.id, position)\n }\n return found ?? NO_POSITIONS\n }\n\n function recompute(): void {\n const editor = current()\n const previous = store.get()\n const user = userNow()\n const available = user !== null\n const canComment = available && ready && canCommentNow()\n const views = threads.map(thread => viewOf(thread, anchors.get(thread.annotationId)))\n const nextRanges = rangesOf(editor)\n const ranges = sameRanges(nextRanges, previous.ranges) ? previous.ranges : nextRanges\n const nextOrphanAt = orphanPositions(editor, ranges)\n const orphanAt = sameOrphanAt(nextOrphanAt, previous.orphanAt) ? previous.orphanAt : nextOrphanAt\n const plugin = editor && commentsKey.getState(editor.state)\n const active = plugin?.active ?? null\n const anchor = plugin?.draft\n const draft = editor && anchor ? resolveRange(editor.state, anchor) : null\n const draftQuote = editor && draft ? editor.state.doc.textBetween(draft.from, draft.to, ' ').slice(0, COMMENT_LIMITS.quote) : ''\n const sameViews = (views.length === previous.threads.length && views.every((view, index) => sameJson(view, previous.threads[index])))\n const threadsOut = sameViews ? previous.threads : views\n\n if (\n available === previous.available && canComment === previous.canComment && sameUser(user, previous.user) && threadsOut === previous.threads\n && ranges === previous.ranges && orphanAt === previous.orphanAt && active === previous.active && sameRange(draft, previous.draft) && draftQuote === previous.draftQuote\n ) return\n\n const kept = threadsOut === previous.threads && ranges === previous.ranges\n store.set({\n available,\n canComment,\n user,\n threads: threadsOut,\n ranges,\n orphanAt,\n active,\n draft: sameRange(draft, previous.draft) ? previous.draft : draft,\n draftQuote,\n open: kept\n ? previous.open\n : threadsOut\n .filter(thread => !thread.resolved)\n .sort((a, b) => (ranges.get(a.id)?.from ?? Infinity) - (ranges.get(b.id)?.from ?? Infinity) || a.createdAt.localeCompare(b.createdAt)),\n resolved: threadsOut === previous.threads ? previous.resolved : threadsOut.filter(thread => thread.resolved).sort((a, b) => b.createdAt.localeCompare(a.createdAt)),\n })\n }\n\n // -- Editor binding -----------------------------------------------------------------------------------------------------\n\n let bound: Editor | undefined\n\n function onTransaction({ transaction, appendedTransactions }: { transaction: Transaction, appendedTransactions?: readonly Transaction[] }): void {\n const editor = bound\n if (editor && anchors.size) {\n const all = [transaction, ...(appendedTransactions ?? [])]\n if (all.some(tr => tr.docChanged)) {\n if (isBlank(editor.state.doc)) unresolved = true\n else if (unresolved) resolveAll(editor)\n else carry(all)\n }\n }\n recompute()\n }\n\n function onDestroy(): void {\n if (bound) gone.add(bound)\n unbind()\n recompute()\n }\n\n function unbind(): void {\n bound?.off('transaction', onTransaction)\n bound?.off('update', recompute)\n bound?.off('destroy', onDestroy)\n bound = undefined\n }\n\n function refresh(): void {\n const editor = current()\n if (editor !== bound) {\n unbind()\n bound = editor\n editor?.on('transaction', onTransaction)\n // `setEditable` emits `update` only, and `canComment` may follow it.\n editor?.on('update', recompute)\n editor?.on('destroy', onDestroy)\n if (editor && loaded) resolveAll(editor)\n }\n if (!loaded) {\n loaded = true\n enqueue(load, false)\n }\n recompute()\n }\n\n function dispose(): void {\n flush()\n }\n\n async function load(): Promise<void> {\n const discussion = await request()\n if (!isDiscussion(discussion)) throw new Error(`Comments request GET ${endpoint} did not answer a discussion.`)\n threads = discussion.threads\n anchors = new Map(discussion.annotations.map((annotation: Annotation) => [annotation.id, annotation.anchor]))\n persisted.clear()\n for (const [id, anchor] of anchors) persisted.set(id, JSON.stringify(anchor))\n dirty.clear()\n deletedAt.clear()\n ready = true\n const editor = current()\n unresolved = anchors.size > 0\n if (editor) resolveAll(editor)\n recompute()\n }\n\n function reload(): Promise<void> {\n flush()\n loaded = true\n enqueue(load, false)\n return idle()\n }\n\n async function idle(): Promise<void> {\n flush()\n const seen = chain\n await seen\n // A request that failed queued a reload, a finished one may have moved anchors.\n if (chain !== seen || timer !== undefined || dirty.size > 0) await idle()\n }\n\n // -- Mutations ----------------------------------------------------------------------------------------------------------\n\n /** Trimmed and cut to the limit; '' when nothing is left. */\n const clean = (text: string) => text.trim().slice(0, COMMENT_LIMITS.text)\n\n function writer(): AuthorIdentity | null {\n const user = userNow()\n return user && ready && canCommentNow() ? user : null\n }\n\n const commentOf = (user: AuthorIdentity, body: string): Comment => ({ id: newId('cm'), body, authorId: user.id, authorName: user.name, authorColor: user.color, createdAt: new Date().toISOString() })\n\n function setThreads(next: readonly CommentThread[]): void {\n threads = next\n recompute()\n }\n\n const threadById = (id: string) => threads.find(thread => thread.id === id)\n\n function replaceThread(id: string, change: (thread: CommentThread) => CommentThread): void {\n setThreads(threads.map(thread => (thread.id === id ? change(thread) : thread)))\n }\n\n function add(text: string): string | null {\n const editor = current()\n const user = writer()\n const body = clean(text)\n const draft = editor && commentsKey.getState(editor.state)?.draft\n const range = editor && draft ? resolveRange(editor.state, draft) : null\n if (!editor || !user || !draft || !range || range.from >= range.to || !body) return null\n const threadId = newId('ct')\n const annotation: Annotation = { id: newId('ca'), threadId, anchor: captureAnchor(editor, range) }\n const comment = commentOf(user, body)\n editor.view.dispatch(editor.state.tr.setMeta(commentsKey, { draft: null, active: threadId }).setMeta('addToHistory', false))\n releaseRanges(editor, [draft])\n anchors.set(annotation.id, annotation.anchor)\n persisted.set(annotation.id, JSON.stringify(annotation.anchor))\n threads = [...threads, { id: threadId, annotationId: annotation.id, status: 'open', comments: [comment] }]\n recompute()\n send({ action: 'create', annotation, thread: { id: threadId, annotationId: annotation.id, initialComment: comment } })\n return threadId\n }\n\n function reply(threadId: string, text: string): void {\n const user = writer()\n const body = clean(text)\n if (!user || !body || !threadById(threadId)) return\n const comment = commentOf(user, body)\n replaceThread(threadId, thread => ({ ...thread, comments: [...thread.comments, comment] }))\n send({ action: 'reply', threadId, comment })\n }\n\n function edit(threadId: string, replyId: string | null, text: string): void {\n const user = writer()\n const body = clean(text)\n const thread = threadById(threadId)\n const target = thread && (replyId === null ? thread.comments[0] : thread.comments.find(comment => comment.id === replyId))\n if (!user || !body || !thread || !target || target.authorId !== user.id || target.body === body) return\n const editedAt = new Date().toISOString()\n replaceThread(threadId, item => ({ ...item, comments: item.comments.map(comment => (comment.id === target.id ? { ...comment, body, editedAt } : comment)) }))\n send({ action: 'edit', threadId, commentId: target.id, body, editedAt })\n }\n\n const activeThread = () => {\n const editor = current()\n return editor ? commentsKey.getState(editor.state)?.active ?? null : null\n }\n\n function setResolved(threadId: string, resolved: boolean): void {\n const thread = threadById(threadId)\n if (!writer() || !thread || (thread.status === 'resolved') === resolved) return\n const status = resolved ? 'resolved' : 'open'\n replaceThread(threadId, item => ({ ...item, status }))\n if (resolved && activeThread() === threadId) activate(null)\n send({ action: 'status', threadId, status })\n }\n\n function remove(threadId: string): void {\n const user = writer()\n const thread = threadById(threadId)\n if (!user || !thread || thread.comments[0]?.authorId !== user.id) return\n if (activeThread() === threadId) activate(null)\n anchors.delete(thread.annotationId)\n persisted.delete(thread.annotationId)\n dirty.delete(thread.annotationId)\n deletedAt.delete(thread.annotationId)\n setThreads(threads.filter(item => item.id !== threadId))\n send({ action: 'remove', threadId })\n }\n\n function removeReply(threadId: string, replyId: string): void {\n const user = writer()\n const thread = threadById(threadId)\n const target = thread?.comments.slice(1).find(comment => comment.id === replyId)\n if (!user || !thread || !target || target.authorId !== user.id) return\n replaceThread(threadId, item => ({ ...item, comments: item.comments.filter(comment => comment.id !== replyId) }))\n send({ action: 'remove-comment', threadId, commentId: replyId })\n }\n\n function activate(id: string | null, activateOptions: { reveal?: boolean } = {}): void {\n const editor = current()\n if (editor) setActiveComment(editor, id, activateOptions)\n }\n\n function startDraft(): void {\n const editor = current()\n if (editor && writer()) startCommentDraft(editor)\n }\n\n function cancelDraft(): void {\n const editor = current()\n if (editor) cancelCommentDraft(editor)\n }\n\n return { state: store, refresh, dispose, add, reply, edit, setResolved, remove, removeReply, activate, startDraft, cancelDraft, reload, idle }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../../../src/adapters/comments/client.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport type { Transaction } from '@tiptap/pm/state'\nimport { type AuthorIdentity, createStore, releaseRanges, resolveRange } from '../../core'\nimport { cancelCommentDraft, COMMENT_LIMITS, type CommentReply, type CommentsModel, type CommentsSnapshot, type CommentThread as EditorThread, commentsKey, type DocRange, setActiveComment, startCommentDraft } from '../../features/comments'\nimport { EMPTY_SNAPSHOT, NO_POSITIONS, NO_RANGES, NO_THREADS, sameOrphanAt, sameRange, sameRanges, sameUser } from '../../features/comments/snapshot-equality'\nimport { captureAnchor, capturePoint, type PortableAnchor, resolveAnchor, mapAnchor } from '../anchors'\nimport type { Annotation, Comment, CommentAction, CommentThread, Discussion } from './types'\n\n// A `CommentsModel` for the editor's comments feature (`commentsFeature({ model })`) whose threads live behind a REST\n// endpoint (`handleCommentsRequest` serves it) instead of the document's Y.Doc. The text a thread hangs on is a\n// portable anchor (`../anchors`), not a `comment` mark: anchors are resolved against the document when the threads\n// load, follow every edit of the editor, and are written back to the endpoint when they moved.\n\n/** The part of a `Response` the model reads; `globalThis.fetch` fits. */\nexport interface CommentsFetchResponse {\n ok: boolean\n status: number\n json(): Promise<unknown>\n}\n\nexport type CommentsFetch = (url: string, init?: { method?: string, headers?: Record<string, string>, body?: string }) => Promise<CommentsFetchResponse>\n\nexport interface RestCommentsModelOptions {\n /** How requests are sent: `globalThis.fetch`, or a wrapper that adds the base URL and credentials. */\n fetch: CommentsFetch\n /** The document the threads belong to. */\n documentId: string\n /** The document's comments endpoint (`GET` → `Discussion`, `POST` → `CommentAction`); default `/comments/<encoded documentId>`. */\n endpoint?: string\n /** Who writes: the author of new comments and the only one who may edit or delete them; `null` while nobody is signed in (no comments then). */\n user: AuthorIdentity | null | (() => AuthorIdentity | null)\n /** Whether the user may compose and change comments now; readers of a read-only document may be allowed. */\n canComment: boolean | (() => boolean)\n /** The editor the threads belong to; may return no editor yet or a destroyed one. */\n getEditor: () => Editor | undefined\n /** Milliseconds a changed anchor waits before it is written back (typing moves many); default 500. `idle()` writes at once. */\n persistDelay?: number\n /** Called with every failed request; the model reloads the threads afterwards, so the local state falls back to the server's. */\n onError?: (error: Error) => void\n}\n\nexport interface RestCommentsModel extends CommentsModel {\n /** Fetches the threads again and resolves their anchors against the current document. */\n reload(): Promise<void>\n /** Resolves once every change made so far has been sent (and the threads loaded, if a load was requested). */\n idle(): Promise<void>\n}\n\n/** Neutral colour of authors that stored no colour. */\nconst FALLBACK_COLOR = '#6b7280'\n/** Characters of context that must agree for an orphan to be re-attached on load. */\nconst REATTACH_CONTEXT = 24\n\nconst newId = (prefix: string) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`\n/** The `error` message of a refused request's JSON body. */\nfunction errorOf(body: unknown): unknown {\n return typeof body === 'object' && body !== null && 'error' in body ? body.error : undefined\n}\n\nfunction isDiscussion(value: unknown): value is Discussion {\n return typeof value === 'object' && value !== null && 'annotations' in value && Array.isArray(value.annotations) && 'threads' in value && Array.isArray(value.threads)\n}\n\nconst sameJson = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b)\n\nconst authorOf = (comment: Comment): EditorThread['author'] => ({ id: comment.authorId, name: comment.authorName ?? comment.authorId, color: comment.authorColor ?? FALLBACK_COLOR })\n\nfunction quoteOf(anchor: PortableAnchor | undefined): string {\n if (!anchor) return ''\n if (anchor.kind === 'range') return anchor.quote\n if (anchor.kind === 'orphan') return anchor.quote ?? (anchor.previous.kind === 'range' ? anchor.previous.quote : '')\n return ''\n}\n\nfunction viewOf(thread: CommentThread, anchor: PortableAnchor | undefined): EditorThread {\n const [first, ...replies] = thread.comments\n return {\n id: thread.id,\n author: first ? authorOf(first) : { id: '', name: '', color: FALLBACK_COLOR },\n text: first?.body ?? '',\n createdAt: first?.createdAt ?? '',\n ...(first?.editedAt !== undefined && { editedAt: first.editedAt }),\n quote: quoteOf(anchor),\n resolved: thread.status === 'resolved',\n replies: replies.map((reply): CommentReply => ({\n id: reply.id,\n author: authorOf(reply),\n text: reply.body,\n createdAt: reply.createdAt,\n ...(reply.editedAt !== undefined && { editedAt: reply.editedAt }),\n })),\n }\n}\n\nfunction rangeOf(anchor: PortableAnchor | undefined): DocRange | null {\n if (!anchor || anchor.kind === 'orphan') return null\n return anchor.kind === 'range' ? { from: anchor.from, to: anchor.to } : { from: anchor.position, to: anchor.position }\n}\n\n/** A document is \"empty\" while the host has not put its content in yet: nothing to resolve anchors against. */\nconst isBlank = (doc: { textContent: string }) => doc.textContent === ''\n\n/** Whether the text at `candidate` still reads like the context `previous` was written in (one side suffices). */\nfunction contextAgrees(candidate: PortableAnchor, previous: PortableAnchor): boolean {\n if (candidate.kind === 'orphan' || previous.kind === 'orphan') return false\n const before = previous.contextBefore?.slice(-REATTACH_CONTEXT)\n const after = previous.contextAfter?.slice(0, REATTACH_CONTEXT)\n if (!before && !after) return true\n return (!!before && !!candidate.contextBefore?.endsWith(before)) || (!!after && !!candidate.contextAfter?.startsWith(after))\n}\n\n/**\n * The comments of one editor over a REST endpoint, as a `CommentsModel` (the host passes `() => model` as the\n * `model` factory of `commentsFeature`/`commentsFeatureCore`; one instance serves the plugin and every hook).\n *\n * - **Load.** `refresh()` (called by the feature once the editor exists) fetches `GET endpoint`; each annotation's\n * anchor is resolved against the document (exact offsets, else quote and context search). An anchor that moved is\n * re-captured and written back; an anchor whose text is gone becomes an `orphan` (the thread stays, without a range)\n * and an orphan whose text is back with the same surroundings is re-attached. While the document is still empty the\n * resolving waits for its content.\n * - **Edits.** Every transaction carries all anchors with it (`mapAnchor`), so ranges, quotes and contexts follow the\n * text; changed anchors are written back after `persistDelay`. Text deleted under an anchor orphans it for good (it is\n * never silently re-attached to other text during editing; `reload()` re-attaches it when its text and context\n * are found again, e.g. after the content was replaced).\n * - **Writes** are applied locally at once and sent in order; a failed request is reported to `onError` and the threads\n * are reloaded. Without a `user` or while `canComment` is false nothing is written. Only the author may edit or\n * delete a comment.\n * - **Lifecycle.** The model follows the editor `getEditor` names until that editor is destroyed; `dispose()` writes\n * pending anchor changes and does not stop it (the feature and its hooks share the instance and dispose it on their\n * own schedule).\n */\nexport function createRestCommentsModel(options: RestCommentsModelOptions): RestCommentsModel {\n const { fetch, documentId, getEditor, onError } = options\n const endpoint = options.endpoint ?? `/comments/${encodeURIComponent(documentId)}`\n const persistDelay = options.persistDelay ?? 500\n const store = createStore<CommentsSnapshot>(EMPTY_SNAPSHOT)\n\n const userNow = () => (typeof options.user === 'function' ? options.user() : options.user)\n const canCommentNow = () => (typeof options.canComment === 'function' ? options.canComment() : options.canComment)\n /** Editors that announced their destruction: their view is still alive while `destroy` is emitted. */\n const gone = new WeakSet<Editor>()\n const current = () => {\n const instance = getEditor()\n return instance && !instance.isDestroyed && !gone.has(instance) ? instance : undefined\n }\n\n // -- Data: what the endpoint holds (annotation anchors are the local truth until written back) -----------------------\n let threads: readonly CommentThread[] = []\n let anchors = new Map<string, PortableAnchor>()\n /** The anchor each annotation has on the server, serialised. */\n const persisted = new Map<string, string>()\n const dirty = new Set<string>()\n /**\n * Where the text of an annotation that became an orphan during this session was: the deletion point, mapped through\n * every later edit. Keyed by annotation id (`orphanAt` in the snapshot is keyed by thread id; `orphanPositions` translates).\n * Deletion points are session-local: `load()` clears them, and an anchor that arrives already orphaned has none, because\n * the server stores no deletion point. Such threads get no `orphanAt` entry and sit after the placed ones in the margin.\n */\n const deletedAt = new Map<string, number>()\n /** Anchors wait for the document content (resolve on load) before they get a range. */\n let unresolved = false\n /** A load was requested / the first one succeeded: writes wait for it, so a load never overwrites a newer local change. */\n let loaded = false\n let ready = false\n\n // -- Requests: one queue, so a reply never overtakes its thread's creation ---------------------------------------------\n let chain: Promise<void> = Promise.resolve()\n let timer: ReturnType<typeof setTimeout> | undefined\n\n function enqueue(task: () => Promise<void>, reloadOnError = true): void {\n chain = chain.then(task).catch((error: unknown) => {\n onError?.(error instanceof Error ? error : new Error(String(error)))\n if (reloadOnError) enqueue(load, false)\n })\n }\n\n async function request(init?: { method: string, body: CommentAction }): Promise<unknown> {\n const response = await fetch(endpoint, init && { method: init.method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(init.body) })\n if (!response.ok) {\n const detail = await response.json().then(errorOf, () => undefined)\n throw new Error(`@ecsplico/editor: comments request ${init?.method ?? 'GET'} ${endpoint} failed: ${response.status}${typeof detail === 'string' ? ` ${detail}` : ''}`)\n }\n return response.json()\n }\n\n const send = (body: CommentAction) => enqueue(async () => void await request({ method: 'POST', body }))\n\n function writeAnchors(): void {\n timer = undefined\n if (!userNow() || !ready || !canCommentNow()) {\n dirty.clear()\n return\n }\n for (const id of dirty) {\n const anchor = anchors.get(id)\n const threadId = threads.find(thread => thread.annotationId === id)?.id\n if (!anchor || !threadId || persisted.get(id) === JSON.stringify(anchor)) continue\n persisted.set(id, JSON.stringify(anchor))\n send({ action: 'update-annotation', annotation: { id, threadId, anchor } })\n }\n dirty.clear()\n }\n\n function markDirty(id: string): void {\n dirty.add(id)\n if (timer === undefined) timer = setTimeout(writeAnchors, persistDelay)\n }\n\n function flush(): void {\n clearTimeout(timer)\n writeAnchors()\n }\n\n // -- Anchors ------------------------------------------------------------------------------------------------------------\n\n /** `anchor` read against `editor`'s document: the moved/re-captured anchor, an orphan, or the same object. */\n function resolveOne(editor: Editor, anchor: PortableAnchor): PortableAnchor {\n const live = anchor.kind === 'orphan' ? anchor.previous : anchor\n const found = resolveAnchor(editor.state.doc, live)\n if (!found) {\n return anchor.kind === 'orphan'\n ? anchor\n : { kind: 'orphan', previous: anchor, quote: quoteOf(anchor) || undefined, contextBefore: anchor.contextBefore, contextAfter: anchor.contextAfter, reason: 'unresolvable' }\n }\n const next = live.kind === 'range' ? captureAnchor(editor, found) : capturePoint(editor, found.from)\n if (anchor.kind === 'orphan' && !contextAgrees(next, live)) return anchor\n return sameJson(next, anchor) ? anchor : next\n }\n\n function resolveAll(editor: Editor): void {\n if (isBlank(editor.state.doc)) {\n unresolved = anchors.size > 0\n return\n }\n unresolved = false\n for (const [id, anchor] of anchors) {\n const next = resolveOne(editor, anchor)\n if (next !== anchor) {\n anchors.set(id, next)\n if (next.kind !== 'orphan') deletedAt.delete(id)\n markDirty(id)\n }\n }\n }\n\n function carry(transactions: readonly Transaction[]): void {\n for (const transaction of transactions) {\n if (!transaction.docChanged) continue\n for (const [id, position] of deletedAt) deletedAt.set(id, transaction.mapping.map(position, 1))\n for (const [id, anchor] of anchors) {\n const next = mapAnchor(anchor, transaction)\n if (next !== anchor) {\n anchors.set(id, next)\n if (next.kind === 'orphan' && anchor.kind !== 'orphan') {\n deletedAt.set(id, transaction.mapping.map(anchor.kind === 'range' ? anchor.from : anchor.position, 1))\n }\n markDirty(id)\n }\n }\n }\n }\n\n // -- Snapshot -----------------------------------------------------------------------------------------------------------\n\n function rangesOf(editor: Editor | undefined): ReadonlyMap<string, DocRange> {\n if (!editor || unresolved) return NO_RANGES\n const size = editor.state.doc.content.size\n const ranges = new Map<string, DocRange>()\n for (const thread of threads) {\n const range = rangeOf(anchors.get(thread.annotationId))\n if (range && range.from <= size) ranges.set(thread.id, { from: range.from, to: Math.min(range.to, size) })\n }\n return ranges\n }\n\n /**\n * The snapshot's `orphanAt`, keyed by thread id: where each open thread without a text range was, read from `deletedAt`\n * (keyed by annotation id). Empty when there is no tracked deletion point; threads loaded already orphaned never have one\n * (the server stores none) and are left out, so they sit after the placed ones in the margin.\n */\n function orphanPositions(editor: Editor | undefined, ranges: ReadonlyMap<string, DocRange>): ReadonlyMap<string, number> {\n if (!editor || unresolved || deletedAt.size === 0) return NO_POSITIONS\n const size = editor.state.doc.content.size\n let found: Map<string, number> | undefined\n for (const thread of threads) {\n if (thread.status === 'resolved' || ranges.has(thread.id)) continue\n const position = deletedAt.get(thread.annotationId)\n if (position !== undefined && position <= size) (found ??= new Map()).set(thread.id, position)\n }\n return found ?? NO_POSITIONS\n }\n\n function recompute(): void {\n const editor = current()\n const previous = store.get()\n const user = userNow()\n const available = user !== null\n const canComment = available && ready && canCommentNow()\n const views = threads.map(thread => viewOf(thread, anchors.get(thread.annotationId)))\n const nextRanges = rangesOf(editor)\n const ranges = sameRanges(nextRanges, previous.ranges) ? previous.ranges : nextRanges\n const nextOrphanAt = orphanPositions(editor, ranges)\n const orphanAt = sameOrphanAt(nextOrphanAt, previous.orphanAt) ? previous.orphanAt : nextOrphanAt\n const plugin = editor && commentsKey.getState(editor.state)\n const active = plugin?.active ?? null\n const anchor = plugin?.draft\n const draft = editor && anchor ? resolveRange(editor.state, anchor) : null\n const draftQuote = editor && draft ? editor.state.doc.textBetween(draft.from, draft.to, ' ').slice(0, COMMENT_LIMITS.quote) : ''\n const sameViews = (views.length === previous.threads.length && views.every((view, index) => sameJson(view, previous.threads[index])))\n const threadsOut = sameViews ? previous.threads : views\n\n if (\n available === previous.available && canComment === previous.canComment && sameUser(user, previous.user) && threadsOut === previous.threads\n && ranges === previous.ranges && orphanAt === previous.orphanAt && active === previous.active && sameRange(draft, previous.draft) && draftQuote === previous.draftQuote\n ) return\n\n const kept = threadsOut === previous.threads && ranges === previous.ranges\n store.set({\n available,\n canComment,\n user,\n threads: threadsOut,\n ranges,\n orphanAt,\n active,\n draft: sameRange(draft, previous.draft) ? previous.draft : draft,\n draftQuote,\n open: kept\n ? previous.open\n : threadsOut\n .filter(thread => !thread.resolved)\n .sort((a, b) => (ranges.get(a.id)?.from ?? Infinity) - (ranges.get(b.id)?.from ?? Infinity) || a.createdAt.localeCompare(b.createdAt)),\n resolved: threadsOut === previous.threads ? previous.resolved : threadsOut.filter(thread => thread.resolved).sort((a, b) => b.createdAt.localeCompare(a.createdAt)),\n })\n }\n\n // -- Editor binding -----------------------------------------------------------------------------------------------------\n\n let bound: Editor | undefined\n\n function onTransaction({ transaction, appendedTransactions }: { transaction: Transaction, appendedTransactions?: readonly Transaction[] }): void {\n const editor = bound\n if (editor && anchors.size) {\n const all = [transaction, ...(appendedTransactions ?? [])]\n if (all.some(tr => tr.docChanged)) {\n if (isBlank(editor.state.doc)) unresolved = true\n else if (unresolved) resolveAll(editor)\n else carry(all)\n }\n }\n recompute()\n }\n\n function onDestroy(): void {\n if (bound) gone.add(bound)\n unbind()\n recompute()\n }\n\n function unbind(): void {\n bound?.off('transaction', onTransaction)\n bound?.off('update', recompute)\n bound?.off('destroy', onDestroy)\n bound = undefined\n }\n\n function refresh(): void {\n const editor = current()\n if (editor !== bound) {\n unbind()\n bound = editor\n editor?.on('transaction', onTransaction)\n // `setEditable` emits `update` only, and `canComment` may follow it.\n editor?.on('update', recompute)\n editor?.on('destroy', onDestroy)\n if (editor && loaded) resolveAll(editor)\n }\n if (!loaded) {\n loaded = true\n enqueue(load, false)\n }\n recompute()\n }\n\n function dispose(): void {\n flush()\n }\n\n async function load(): Promise<void> {\n const discussion = await request()\n if (!isDiscussion(discussion)) throw new Error(`@ecsplico/editor: comments request GET ${endpoint} did not answer a discussion`)\n threads = discussion.threads\n anchors = new Map(discussion.annotations.map((annotation: Annotation) => [annotation.id, annotation.anchor]))\n persisted.clear()\n for (const [id, anchor] of anchors) persisted.set(id, JSON.stringify(anchor))\n dirty.clear()\n deletedAt.clear()\n ready = true\n const editor = current()\n unresolved = anchors.size > 0\n if (editor) resolveAll(editor)\n recompute()\n }\n\n function reload(): Promise<void> {\n flush()\n loaded = true\n enqueue(load, false)\n return idle()\n }\n\n async function idle(): Promise<void> {\n flush()\n const seen = chain\n await seen\n // A request that failed queued a reload, a finished one may have moved anchors.\n if (chain !== seen || timer !== undefined || dirty.size > 0) await idle()\n }\n\n // -- Mutations ----------------------------------------------------------------------------------------------------------\n\n /** Trimmed and cut to the limit; '' when nothing is left. */\n const clean = (text: string) => text.trim().slice(0, COMMENT_LIMITS.text)\n\n function writer(): AuthorIdentity | null {\n const user = userNow()\n return user && ready && canCommentNow() ? user : null\n }\n\n const commentOf = (user: AuthorIdentity, body: string): Comment => ({ id: newId('cm'), body, authorId: user.id, authorName: user.name, authorColor: user.color, createdAt: new Date().toISOString() })\n\n function setThreads(next: readonly CommentThread[]): void {\n threads = next\n recompute()\n }\n\n const threadById = (id: string) => threads.find(thread => thread.id === id)\n\n function replaceThread(id: string, change: (thread: CommentThread) => CommentThread): void {\n setThreads(threads.map(thread => (thread.id === id ? change(thread) : thread)))\n }\n\n function add(text: string): string | null {\n const editor = current()\n const user = writer()\n const body = clean(text)\n const draft = editor && commentsKey.getState(editor.state)?.draft\n const range = editor && draft ? resolveRange(editor.state, draft) : null\n if (!editor || !user || !draft || !range || range.from >= range.to || !body) return null\n const threadId = newId('ct')\n const annotation: Annotation = { id: newId('ca'), threadId, anchor: captureAnchor(editor, range) }\n const comment = commentOf(user, body)\n editor.view.dispatch(editor.state.tr.setMeta(commentsKey, { draft: null, active: threadId }).setMeta('addToHistory', false))\n releaseRanges(editor, [draft])\n anchors.set(annotation.id, annotation.anchor)\n persisted.set(annotation.id, JSON.stringify(annotation.anchor))\n threads = [...threads, { id: threadId, annotationId: annotation.id, status: 'open', comments: [comment] }]\n recompute()\n send({ action: 'create', annotation, thread: { id: threadId, annotationId: annotation.id, initialComment: comment } })\n return threadId\n }\n\n function reply(threadId: string, text: string): void {\n const user = writer()\n const body = clean(text)\n if (!user || !body || !threadById(threadId)) return\n const comment = commentOf(user, body)\n replaceThread(threadId, thread => ({ ...thread, comments: [...thread.comments, comment] }))\n send({ action: 'reply', threadId, comment })\n }\n\n function edit(threadId: string, replyId: string | null, text: string): void {\n const user = writer()\n const body = clean(text)\n const thread = threadById(threadId)\n const target = thread && (replyId === null ? thread.comments[0] : thread.comments.find(comment => comment.id === replyId))\n if (!user || !body || !thread || !target || target.authorId !== user.id || target.body === body) return\n const editedAt = new Date().toISOString()\n replaceThread(threadId, item => ({ ...item, comments: item.comments.map(comment => (comment.id === target.id ? { ...comment, body, editedAt } : comment)) }))\n send({ action: 'edit', threadId, commentId: target.id, body, editedAt })\n }\n\n const activeThread = () => {\n const editor = current()\n return editor ? commentsKey.getState(editor.state)?.active ?? null : null\n }\n\n function setResolved(threadId: string, resolved: boolean): void {\n const thread = threadById(threadId)\n if (!writer() || !thread || (thread.status === 'resolved') === resolved) return\n const status = resolved ? 'resolved' : 'open'\n replaceThread(threadId, item => ({ ...item, status }))\n if (resolved && activeThread() === threadId) activate(null)\n send({ action: 'status', threadId, status })\n }\n\n function remove(threadId: string): void {\n const user = writer()\n const thread = threadById(threadId)\n if (!user || !thread || thread.comments[0]?.authorId !== user.id) return\n if (activeThread() === threadId) activate(null)\n anchors.delete(thread.annotationId)\n persisted.delete(thread.annotationId)\n dirty.delete(thread.annotationId)\n deletedAt.delete(thread.annotationId)\n setThreads(threads.filter(item => item.id !== threadId))\n send({ action: 'remove', threadId })\n }\n\n function removeReply(threadId: string, replyId: string): void {\n const user = writer()\n const thread = threadById(threadId)\n const target = thread?.comments.slice(1).find(comment => comment.id === replyId)\n if (!user || !thread || !target || target.authorId !== user.id) return\n replaceThread(threadId, item => ({ ...item, comments: item.comments.filter(comment => comment.id !== replyId) }))\n send({ action: 'remove-comment', threadId, commentId: replyId })\n }\n\n function activate(id: string | null, activateOptions: { reveal?: boolean } = {}): void {\n const editor = current()\n if (editor) setActiveComment(editor, id, activateOptions)\n }\n\n function startDraft(): void {\n const editor = current()\n if (editor && writer()) startCommentDraft(editor)\n }\n\n function cancelDraft(): void {\n const editor = current()\n if (editor) cancelCommentDraft(editor)\n }\n\n return { state: store, refresh, dispose, add, reply, edit, setResolved, remove, removeReply, activate, startDraft, cancelDraft, reload, idle }\n}\n"],"mappings":"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"}
|
|
@@ -96,7 +96,7 @@ function createDrizzleCommentStore({ db, tables }) {
|
|
|
96
96
|
}
|
|
97
97
|
async function requireThread(documentId, id) {
|
|
98
98
|
const thread = await threadOf(documentId, id);
|
|
99
|
-
if (!thread) throw notFound("
|
|
99
|
+
if (!thread) throw notFound("Kommentar-Thread", id);
|
|
100
100
|
return thread;
|
|
101
101
|
}
|
|
102
102
|
async function discussion(documentId) {
|
|
@@ -146,8 +146,8 @@ function createDrizzleCommentStore({ db, tables }) {
|
|
|
146
146
|
createThread: (documentId, input) => write(async () => {
|
|
147
147
|
assertNewThread(input);
|
|
148
148
|
const { annotation, thread } = input;
|
|
149
|
-
if (await db.select({ id: annotations.id }).from(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).get()) throw conflict(`Annotation
|
|
150
|
-
if (await threadRow(documentId, thread.id)) throw conflict(`
|
|
149
|
+
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}“ existiert bereits`);
|
|
150
|
+
if (await threadRow(documentId, thread.id)) throw conflict(`Kommentar-Thread „${thread.id}“ existiert bereits`);
|
|
151
151
|
try {
|
|
152
152
|
await db.insert(annotations).values({
|
|
153
153
|
documentId,
|
|
@@ -177,12 +177,12 @@ function createDrizzleCommentStore({ db, tables }) {
|
|
|
177
177
|
}),
|
|
178
178
|
addComment: (documentId, threadId, comment) => write(async () => {
|
|
179
179
|
await requireThread(documentId, threadId);
|
|
180
|
-
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()) throw conflict(`
|
|
180
|
+
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()) throw conflict(`Kommentar „${comment.id}“ existiert bereits im Thread „${threadId}“`);
|
|
181
181
|
await insertComment(documentId, threadId, comment);
|
|
182
182
|
return requireThread(documentId, threadId);
|
|
183
183
|
}),
|
|
184
184
|
editComment: (documentId, threadId, commentId, body, editedAt) => write(async () => {
|
|
185
|
-
if (!(await requireThread(documentId, threadId)).comments.some((comment) => comment.id === commentId)) throw notFound("
|
|
185
|
+
if (!(await requireThread(documentId, threadId)).comments.some((comment) => comment.id === commentId)) throw notFound("Kommentar", commentId);
|
|
186
186
|
await db.update(comments).set({
|
|
187
187
|
body,
|
|
188
188
|
editedAt
|
|
@@ -191,8 +191,8 @@ function createDrizzleCommentStore({ db, tables }) {
|
|
|
191
191
|
}),
|
|
192
192
|
removeComment: (documentId, threadId, commentId) => write(async () => {
|
|
193
193
|
const index = (await requireThread(documentId, threadId)).comments.findIndex((comment) => comment.id === commentId);
|
|
194
|
-
if (index < 0) throw notFound("
|
|
195
|
-
if (index === 0) throw invalid("
|
|
194
|
+
if (index < 0) throw notFound("Kommentar", commentId);
|
|
195
|
+
if (index === 0) throw invalid("Der erste Kommentar eines Threads lässt sich nur mit dem ganzen Thread entfernen");
|
|
196
196
|
await db.delete(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId), eq(comments.id, commentId))).run();
|
|
197
197
|
return requireThread(documentId, threadId);
|
|
198
198
|
}),
|
|
@@ -203,8 +203,8 @@ function createDrizzleCommentStore({ db, tables }) {
|
|
|
203
203
|
}),
|
|
204
204
|
updateAnchor: (documentId, annotation) => write(async () => {
|
|
205
205
|
const row = await db.select().from(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).get();
|
|
206
|
-
if (!row) throw notFound("
|
|
207
|
-
if (row.threadId !== annotation.threadId) throw invalid("
|
|
206
|
+
if (!row) throw notFound("Annotation", annotation.id);
|
|
207
|
+
if (row.threadId !== annotation.threadId) throw invalid("Eine Annotation kann nicht in einen anderen Thread wechseln");
|
|
208
208
|
await db.update(annotations).set({ anchor: annotation.anchor }).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).run();
|
|
209
209
|
return structuredClone(annotation);
|
|
210
210
|
}),
|
|
@@ -1 +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":";;;;;AAQA,MAAM,iBAAiB;CACrB,aAAa,YAAY,8BAA8B;EACrD,YAAY,KAAK,aAAa,CAAC,CAAC,QAAQ;EACxC,IAAI,KAAK,IAAI,CAAC,CAAC,QAAQ;EACvB,UAAU,KAAK,WAAW,CAAC,CAAC,QAAQ;EACpC,KAAK,QAAQ,KAAK,CAAC,CAAC,QAAQ;EAC5B,QAAQ,KAAK,UAAU,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,MAAsB,CAAC,CAAC,QAAQ;CAC3E,IAAG,UAAS,CAAC,WAAW,EAAE,SAAS,CAAC,MAAM,YAAY,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;CACnE,SAAS,YAAY,0BAA0B;EAC7C,YAAY,KAAK,aAAa,CAAC,CAAC,QAAQ;EACxC,IAAI,KAAK,IAAI,CAAC,CAAC,QAAQ;EACvB,cAAc,KAAK,eAAe,CAAC,CAAC,QAAQ;EAC5C,QAAQ,KAAK,UAAU,EAAE,MAAM,CAAC,QAAQ,UAAU,EAAE,CAAC,CAAC,CAAC,QAAQ;EAC/D,KAAK,QAAQ,KAAK,CAAC,CAAC,QAAQ;CAC9B,IAAG,UAAS,CAAC,WAAW,EAAE,SAAS,CAAC,MAAM,YAAY,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;CACnE,UAAU,YAAY,mBAAmB;EACvC,YAAY,KAAK,aAAa,CAAC,CAAC,QAAQ;EACxC,UAAU,KAAK,WAAW,CAAC,CAAC,QAAQ;EACpC,IAAI,KAAK,IAAI,CAAC,CAAC,QAAQ;EACvB,KAAK,QAAQ,KAAK,CAAC,CAAC,QAAQ;EAC5B,MAAM,KAAK,MAAM,CAAC,CAAC,QAAQ;EAC3B,UAAU,KAAK,WAAW,CAAC,CAAC,QAAQ;EACpC,YAAY,KAAK,aAAa;EAC9B,aAAa,KAAK,cAAc;EAChC,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;EACtC,UAAU,KAAK,WAAW;CAC5B,IAAG,UAAS,CAAC,WAAW,EAAE,SAAS;EAAC,MAAM;EAAY,MAAM;EAAU,MAAM;CAAE,EAAE,CAAC,CAAC,CAAC;AACrF;;;;;;;;;;;AAcA,MAAa,sBAAqC;;AAGlD,eAAsB,uBAAuB,IAA6B;CACxE,MAAM,GAAG,IAAI,GAAG,yMAAyM;CACzN,MAAM,GAAG,IAAI,GAAG,yMAAyM;CACzN,MAAM,GAAG,IAAI,GAAG,+SAA+S;AACjU;;AAQA,MAAM,yBAAS,IAAI,QAAkC;AAErD,SAAS,WAAc,IAAY,QAAsC;CACvE,MAAM,OAAO,OAAO,IAAI,EAAE,KAAK,QAAQ,QAAQ,EAAA,CAAG,KAAK,MAAM;CAC7D,MAAM,OAAO,IAAI,YAAY,CAAC,CAAC;CAC/B,OAAO,IAAI,IAAI,IAAI;CACnB,KAAU,WAAW;EACnB,IAAI,OAAO,IAAI,EAAE,MAAM,MAAM,OAAO,OAAO,EAAE;CAC/C,CAAC;CACD,OAAO;AACT;;;;;;;AAQA,SAAgB,0BAA0B,EAAE,IAAI,UAAoD;CAClG,MAAM,EAAE,aAAa,SAAS,aAAa;CAG3C,MAAM,aAAa,SAA8B;EAC/C,IAAI,IAAI;EACR,MAAM,IAAI;EACV,UAAU,IAAI;EACd,WAAW,IAAI;EACf,GAAI,IAAI,eAAe,QAAQ,EAAE,YAAY,IAAI,WAAW;EAC5D,GAAI,IAAI,gBAAgB,QAAQ,EAAE,aAAa,IAAI,YAAY;EAC/D,GAAI,IAAI,aAAa,QAAQ,EAAE,UAAU,IAAI,SAAS;CACxD;CAEA,eAAe,UAAU,YAAoB,IAAY;EACvD,OAAO,GAAG,OAAO,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,QAAQ,YAAY,UAAU,GAAG,GAAG,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;CAC1G;CAEA,eAAe,SAAS,YAAoB,IAAgD;EAC1F,MAAM,MAAM,MAAM,UAAU,YAAY,EAAE;EAC1C,IAAI,CAAC,KAAK,OAAO,KAAA;EACjB,MAAM,OAAO,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,IAAI,GAAG,SAAS,YAAY,UAAU,GAAG,GAAG,SAAS,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,SAAS,GAAG,CAAC,CAAC,IAAI;EACnJ,OAAO;GAAE,IAAI,IAAI;GAAI,cAAc,IAAI;GAAc,QAAQ,IAAI;GAAQ,UAAU,KAAK,IAAI,SAAS;EAAE;CACzG;CAEA,eAAe,cAAc,YAAoB,IAAoC;EACnF,MAAM,SAAS,MAAM,SAAS,YAAY,EAAE;EAC5C,IAAI,CAAC,QAAQ,MAAM,SAAS,kBAAkB,EAAE;EAChD,OAAO;CACT;CAEA,eAAe,WAAW,YAAyC;EACjE,MAAM,CAAC,gBAAgB,YAAY,eAAe,MAAM,QAAQ,IAAI;GAClE,GAAG,OAAO,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,MAAM,GAAG,YAAY,YAAY,UAAU,CAAC,CAAC,CAAC,QAAQ,YAAY,GAAG,CAAC,CAAC,IAAI;GACzG,GAAG,OAAO,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,MAAM,GAAG,QAAQ,YAAY,UAAU,CAAC,CAAC,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAAC,IAAI;GAC7F,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,GAAG,SAAS,YAAY,UAAU,CAAC,CAAC,CAAC,QAAQ,SAAS,GAAG,CAAC,CAAC,IAAI;EAClG,CAAC;EACD,OAAO;GACL,aAAa,eAAe,KAAI,SAAQ;IAAE,IAAI,IAAI;IAAI,UAAU,IAAI;IAAU,QAAQ,IAAI;GAAO,EAAE;GACnG,SAAS,WAAW,KAAI,SAAQ;IAC9B,IAAI,IAAI;IACR,cAAc,IAAI;IAClB,QAAQ,IAAI;IACZ,UAAU,YAAY,QAAO,YAAW,QAAQ,aAAa,IAAI,EAAE,CAAC,CAAC,IAAI,SAAS;GACpF,EAAE;EACJ;CACF;CAEA,MAAM,UAAU,OAAO,QAAsB,OAAoB,UAA4C;EAE3G,SAAQ,MADU,GAAG,OAAO,EAAE,OAAO,GAAW,gBAAgB,OAAO,OAAO,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,IAAI,EAAA,EACjG,SAAS,KAAK;CAC7B;CAEA,MAAM,gBAAgB,OAAO,YAAoB,UAAkB,YAAqB;EACtF,MAAM,GAAG,OAAO,QAAQ,CAAC,CAAC,OAAO;GAC/B;GACA;GACA,IAAI,QAAQ;GACZ,KAAK,MAAM,QAAQ,SAAS,KAAK,UAAU,IAAI,GAAG,SAAS,YAAY,UAAU,GAAG,GAAG,SAAS,UAAU,QAAQ,CAAC,CAAC;GACpH,MAAM,QAAQ;GACd,UAAU,QAAQ;GAClB,YAAY,QAAQ,cAAc;GAClC,aAAa,QAAQ,eAAe;GACpC,WAAW,QAAQ;GACnB,UAAU,QAAQ,YAAY;EAChC,CAAC,CAAC,CAAC,IAAI;CACT;CAEA,MAAM,SAAY,WAA6B,WAAW,IAAI,MAAM;CAEpE,OAAO;EACL,eAAe;EACf,YAAY,YAAY,aAAa,SAAS,YAAY,QAAQ;EAClE,MAAM,YAAY,YAAY;GAC5B,QAAQ,MAAM,WAAW,UAAU,EAAA,CAAG;EACxC;EAEA,eAAe,YAAY,UAAU,MAAM,YAAY;GACrD,gBAAgB,KAAK;GACrB,MAAM,EAAE,YAAY,WAAW;GAC/B,IAAI,MAAM,GAAG,OAAO,EAAE,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,MAAM,IAAI,GAAG,YAAY,YAAY,UAAU,GAAG,GAAG,YAAY,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,MAAM,SAAS,eAAe,WAAW,GAAG,kBAAkB;GACzN,IAAI,MAAM,UAAU,YAAY,OAAO,EAAE,GAAG,MAAM,SAAS,mBAAmB,OAAO,GAAG,kBAAkB;GAC1G,IAAI;IACF,MAAM,GAAG,OAAO,WAAW,CAAC,CAAC,OAAO;KAClC;KACA,IAAI,WAAW;KACf,UAAU,WAAW;KACrB,KAAK,MAAM,QAAQ,YAAY,KAAK,aAAa,GAAG,YAAY,YAAY,UAAU,CAAC;KACvF,QAAQ,WAAW;IACrB,CAAC,CAAC,CAAC,IAAI;IACP,MAAM,GAAG,OAAO,OAAO,CAAC,CAAC,OAAO;KAC9B;KACA,IAAI,OAAO;KACX,cAAc,OAAO;KACrB,QAAQ;KACR,KAAK,MAAM,QAAQ,QAAQ,KAAK,SAAS,GAAG,QAAQ,YAAY,UAAU,CAAC;IAC7E,CAAC,CAAC,CAAC,IAAI;IACP,MAAM,cAAc,YAAY,OAAO,IAAI,OAAO,cAAc;GAClE,SACO,OAAO;IACZ,MAAM,GAAG,OAAO,QAAQ,CAAC,CAAC,MAAM,IAAI,GAAG,SAAS,YAAY,UAAU,GAAG,GAAG,SAAS,UAAU,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;IAChH,MAAM,GAAG,OAAO,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,QAAQ,YAAY,UAAU,GAAG,GAAG,QAAQ,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;IACvG,MAAM,GAAG,OAAO,WAAW,CAAC,CAAC,MAAM,IAAI,GAAG,YAAY,YAAY,UAAU,GAAG,GAAG,YAAY,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;IACvH,MAAM;GACR;GACA,OAAO;IAAE,YAAY,gBAAgB,UAAU;IAAG,QAAQ,MAAM,cAAc,YAAY,OAAO,EAAE;GAAE;EACvG,CAAC;EAED,aAAa,YAAY,UAAU,YAAY,MAAM,YAAY;GAC/D,MAAM,cAAc,YAAY,QAAQ;GACxC,IAAI,MAAM,GAAG,OAAO,EAAE,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,IAAI,GAAG,SAAS,YAAY,UAAU,GAAG,GAAG,SAAS,UAAU,QAAQ,GAAG,GAAG,SAAS,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GACxK,MAAM,SAAS,YAAY,QAAQ,GAAG,8BAA8B,SAAS,GAAG;GAElF,MAAM,cAAc,YAAY,UAAU,OAAO;GACjD,OAAO,cAAc,YAAY,QAAQ;EAC3C,CAAC;EAED,cAAc,YAAY,UAAU,WAAW,MAAM,aAAa,MAAM,YAAY;GAElF,IAAI,EAAC,MADgB,cAAc,YAAY,QAAQ,EAAA,CAC3C,SAAS,MAAK,YAAW,QAAQ,OAAO,SAAS,GAAG,MAAM,SAAS,WAAW,SAAS;GACnG,MAAM,GAAG,OAAO,QAAQ,CAAC,CAAC,IAAI;IAAE;IAAM;GAAS,CAAC,CAAC,CAAC,MAAM,IAAI,GAAG,SAAS,YAAY,UAAU,GAAG,GAAG,SAAS,UAAU,QAAQ,GAAG,GAAG,SAAS,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI;GACnK,OAAO,cAAc,YAAY,QAAQ;EAC3C,CAAC;EAED,gBAAgB,YAAY,UAAU,cAAc,MAAM,YAAY;GAEpE,MAAM,SAAQ,MADO,cAAc,YAAY,QAAQ,EAAA,CAClC,SAAS,WAAU,YAAW,QAAQ,OAAO,SAAS;GAC3E,IAAI,QAAQ,GAAG,MAAM,SAAS,WAAW,SAAS;GAClD,IAAI,UAAU,GAAG,MAAM,QAAQ,gFAAgF;GAC/G,MAAM,GAAG,OAAO,QAAQ,CAAC,CAAC,MAAM,IAAI,GAAG,SAAS,YAAY,UAAU,GAAG,GAAG,SAAS,UAAU,QAAQ,GAAG,GAAG,SAAS,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI;GAC3I,OAAO,cAAc,YAAY,QAAQ;EAC3C,CAAC;EAED,kBAAkB,YAAY,UAAU,WAAW,MAAM,YAAY;GACnE,MAAM,cAAc,YAAY,QAAQ;GACxC,MAAM,GAAG,OAAO,OAAO,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,IAAI,GAAG,QAAQ,YAAY,UAAU,GAAG,GAAG,QAAQ,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI;GACtH,OAAO,cAAc,YAAY,QAAQ;EAC3C,CAAC;EAED,eAAe,YAAY,eAA2B,MAAM,YAAY;GACtE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,MAAM,IAAI,GAAG,YAAY,YAAY,UAAU,GAAG,GAAG,YAAY,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;GAC1I,IAAI,CAAC,KAAK,MAAM,SAAS,cAAc,WAAW,EAAE;GACpD,IAAI,IAAI,aAAa,WAAW,UAAU,MAAM,QAAQ,8CAA8C;GACtG,MAAM,GAAG,OAAO,WAAW,CAAC,CAAC,IAAI,EAAE,QAAQ,WAAW,OAAO,CAAC,CAAC,CAAC,MAAM,IAAI,GAAG,YAAY,YAAY,UAAU,GAAG,GAAG,YAAY,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;GAC1J,OAAO,gBAAgB,UAAU;EACnC,CAAC;EAED,eAAe,YAAY,aAAa,MAAM,YAAY;GACxD,MAAM,GAAG,OAAO,QAAQ,CAAC,CAAC,MAAM,IAAI,GAAG,SAAS,YAAY,UAAU,GAAG,GAAG,SAAS,UAAU,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI;GAC/G,MAAM,GAAG,OAAO,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,QAAQ,YAAY,UAAU,GAAG,GAAG,QAAQ,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI;GACtG,MAAM,GAAG,OAAO,WAAW,CAAC,CAAC,MAAM,IAAI,GAAG,YAAY,YAAY,UAAU,GAAG,GAAG,YAAY,UAAU,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI;EAC1H,CAAC;EAED,kBAAiB,WAAU,MAAM,YAAY;GAC3C,MAAM,SAAS,WAA6B,GAAG,UAAU,IAAI,WAAW,OAAO,IAAI,EAAE,OAAO,OAAO,OAAO,MAAM;GAChH,MAAM,4BAAY,IAAI,IAAY;GAClC,KAAK,MAAM,CAAC,OAAO,WAAW;IAAC,CAAC,aAAa,YAAY,UAAU;IAAG,CAAC,SAAS,QAAQ,UAAU;IAAG,CAAC,UAAU,SAAS,UAAU;GAAC,GAClI,KAAK,MAAM,OAAO,MAAM,GAAG,eAAe,EAAE,YAAY,OAAO,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,MAAM,MAAM,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,UAAU,IAAI,IAAI,UAAU;GAExI,KAAK,MAAM,CAAC,OAAO,WAAW;IAAC,CAAC,UAAU,SAAS,UAAU;IAAG,CAAC,SAAS,QAAQ,UAAU;IAAG,CAAC,aAAa,YAAY,UAAU;GAAC,GAClI,MAAM,GAAG,OAAO,KAAK,CAAC,CAAC,MAAM,MAAM,MAAM,CAAC,CAAC,CAAC,IAAI;GAElD,OAAO,UAAU;EACnB,CAAC;CACH;AACF"}
|
|
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('Kommentar-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}“ existiert bereits`)\n if (await threadRow(documentId, thread.id)) throw conflict(`Kommentar-Thread „${thread.id}“ existiert bereits`)\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(`Kommentar „${comment.id}“ existiert bereits im 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('Kommentar', 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('Kommentar', commentId)\n if (index === 0) throw invalid('Der erste Kommentar eines Threads lässt sich nur mit dem ganzen Thread entfernen')\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('Eine Annotation kann nicht in einen anderen Thread wechseln')\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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"}
|
|
@@ -10,7 +10,7 @@ var CommentStoreError = class extends Error {
|
|
|
10
10
|
this.name = "CommentStoreError";
|
|
11
11
|
}
|
|
12
12
|
};
|
|
13
|
-
const notFound = (what, id) => new CommentStoreError(
|
|
13
|
+
const notFound = (what, id) => new CommentStoreError(`${what} „${id}“ nicht gefunden`, "not-found", 404);
|
|
14
14
|
const conflict = (message) => new CommentStoreError(message, "conflict", 409);
|
|
15
15
|
const invalid = (message) => new CommentStoreError(message, "invalid", 400);
|
|
16
16
|
//#endregion
|
|
@@ -1 +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(
|
|
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\n// i18n-ignore: HTTP error body of the comments route (server messages stay German, like src/server)\nexport const notFound = (what: string, id: string) => new CommentStoreError(`${what} „${id}“ nicht gefunden`, '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;AAGA,MAAa,YAAY,MAAc,OAAe,IAAI,kBAAkB,GAAG,KAAK,IAAI,GAAG,mBAAmB,aAAa,GAAG;AAC9H,MAAa,YAAY,YAAoB,IAAI,kBAAkB,SAAS,YAAY,GAAG;AAC3F,MAAa,WAAW,YAAoB,IAAI,kBAAkB,SAAS,WAAW,GAAG"}
|
|
@@ -6,18 +6,18 @@ const emptyDiscussion = () => ({
|
|
|
6
6
|
});
|
|
7
7
|
function requireThread(record, id) {
|
|
8
8
|
const thread = record.threads.find((item) => item.id === id);
|
|
9
|
-
if (!thread) throw notFound("
|
|
9
|
+
if (!thread) throw notFound("Kommentar-Thread", id);
|
|
10
10
|
return thread;
|
|
11
11
|
}
|
|
12
12
|
/** Checks the parts of a new thread that do not depend on stored data. */
|
|
13
13
|
function assertNewThread({ annotation, thread }) {
|
|
14
|
-
if (!annotation.id || !thread.id) throw invalid("Annotation
|
|
15
|
-
if (thread.annotationId !== annotation.id || annotation.threadId !== thread.id) throw invalid("
|
|
14
|
+
if (!annotation.id || !thread.id) throw invalid("Annotation und Thread brauchen eine ID");
|
|
15
|
+
if (thread.annotationId !== annotation.id || annotation.threadId !== thread.id) throw invalid("Annotation und Thread müssen aufeinander verweisen");
|
|
16
16
|
}
|
|
17
17
|
function createThread(record, input) {
|
|
18
18
|
assertNewThread(input);
|
|
19
|
-
if (record.annotations.some((item) => item.id === input.annotation.id)) throw conflict(`Annotation
|
|
20
|
-
if (record.threads.some((item) => item.id === input.thread.id)) throw conflict(`
|
|
19
|
+
if (record.annotations.some((item) => item.id === input.annotation.id)) throw conflict(`Annotation „${input.annotation.id}“ existiert bereits`);
|
|
20
|
+
if (record.threads.some((item) => item.id === input.thread.id)) throw conflict(`Kommentar-Thread „${input.thread.id}“ existiert bereits`);
|
|
21
21
|
const annotation = structuredClone(input.annotation);
|
|
22
22
|
const thread = {
|
|
23
23
|
id: input.thread.id,
|
|
@@ -34,14 +34,14 @@ function createThread(record, input) {
|
|
|
34
34
|
}
|
|
35
35
|
function addComment(record, threadId, comment) {
|
|
36
36
|
const thread = requireThread(record, threadId);
|
|
37
|
-
if (thread.comments.some((existing) => existing.id === comment.id)) throw conflict(`
|
|
37
|
+
if (thread.comments.some((existing) => existing.id === comment.id)) throw conflict(`Kommentar „${comment.id}“ existiert bereits im Thread „${threadId}“`);
|
|
38
38
|
thread.comments.push(structuredClone(comment));
|
|
39
39
|
return thread;
|
|
40
40
|
}
|
|
41
41
|
function editComment(record, threadId, commentId, body, editedAt) {
|
|
42
42
|
const thread = requireThread(record, threadId);
|
|
43
43
|
const comment = thread.comments.find((existing) => existing.id === commentId);
|
|
44
|
-
if (!comment) throw notFound("
|
|
44
|
+
if (!comment) throw notFound("Kommentar", commentId);
|
|
45
45
|
comment.body = body;
|
|
46
46
|
comment.editedAt = editedAt;
|
|
47
47
|
return thread;
|
|
@@ -49,8 +49,8 @@ function editComment(record, threadId, commentId, body, editedAt) {
|
|
|
49
49
|
function removeComment(record, threadId, commentId) {
|
|
50
50
|
const thread = requireThread(record, threadId);
|
|
51
51
|
const index = thread.comments.findIndex((existing) => existing.id === commentId);
|
|
52
|
-
if (index < 0) throw notFound("
|
|
53
|
-
if (index === 0) throw invalid("
|
|
52
|
+
if (index < 0) throw notFound("Kommentar", commentId);
|
|
53
|
+
if (index === 0) throw invalid("Der erste Kommentar eines Threads lässt sich nur mit dem ganzen Thread entfernen");
|
|
54
54
|
thread.comments.splice(index, 1);
|
|
55
55
|
return thread;
|
|
56
56
|
}
|
|
@@ -62,8 +62,8 @@ function setThreadStatus(record, threadId, status) {
|
|
|
62
62
|
function updateAnchor(record, annotation) {
|
|
63
63
|
const index = record.annotations.findIndex((item) => item.id === annotation.id);
|
|
64
64
|
const existing = record.annotations[index];
|
|
65
|
-
if (!existing) throw notFound("
|
|
66
|
-
if (existing.threadId !== annotation.threadId) throw invalid("
|
|
65
|
+
if (!existing) throw notFound("Annotation", annotation.id);
|
|
66
|
+
if (existing.threadId !== annotation.threadId) throw invalid("Eine Annotation kann nicht in einen anderen Thread wechseln");
|
|
67
67
|
const stored = structuredClone(annotation);
|
|
68
68
|
record.annotations[index] = stored;
|
|
69
69
|
return stored;
|
|
@@ -1 +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('
|
|
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('Kommentar-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 und Thread brauchen eine ID')\n if (thread.annotationId !== annotation.id || annotation.threadId !== thread.id) throw invalid('Annotation und Thread müssen aufeinander verweisen')\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}“ existiert bereits`)\n if (record.threads.some(item => item.id === input.thread.id)) throw conflict(`Kommentar-Thread „${input.thread.id}“ existiert bereits`)\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(`Kommentar „${comment.id}“ existiert bereits im 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('Kommentar', 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('Kommentar', commentId)\n if (index === 0) throw invalid('Der erste Kommentar eines Threads lässt sich nur mit dem ganzen Thread entfernen')\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('Eine Annotation kann nicht in einen anderen Thread wechseln')\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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"}
|