Comments: data model and API #91

Closed
opened 2026-07-04 14:52:49 +02:00 by fable-5 · 1 comment
Collaborator

Context

Kickoff put comments in scope: discussions attached to pages, with thread and resolve semantics.

Scope

Prisma models comments (thread via parent_id, optional anchor, resolved_at) per data-model.md; endpoints: list per page (threaded, resolved filter), create (Markdown body, sanitized render via shared pipeline), edit/delete own (admins: delete any), resolve/unresolve; permission: reading follows page read; writing requires page read + the pond setting 'who may comment' (readers-and-up | editors-only — add the setting).

Acceptance criteria

  • thread CRUD works with the permission matrix (readers can comment when allowed, not when editors-only)
  • resolved threads are filterable and default-collapsed in the list response
  • comment bodies render through the same escaping pipeline as pages (no HTML injection — fixture test)
  • deleting a page (trash) hides its comments; purge removes them

Technical notes

  • data-model.md §Comments, permissions.md §Non-page objects.

Dependencies

Depends on #52.

Size: ~1.5 days


Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add de and en), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.

## Context Kickoff put comments in scope: discussions attached to pages, with thread and resolve semantics. ## Scope Prisma models `comments` (thread via parent_id, optional anchor, resolved_at) per data-model.md; endpoints: list per page (threaded, resolved filter), create (Markdown body, sanitized render via shared pipeline), edit/delete own (admins: delete any), resolve/unresolve; permission: reading follows page read; writing requires page read + the pond setting 'who may comment' (readers-and-up | editors-only — add the setting). ## Acceptance criteria - [ ] thread CRUD works with the permission matrix (readers can comment when allowed, not when editors-only) - [ ] resolved threads are filterable and default-collapsed in the list response - [ ] comment bodies render through the same escaping pipeline as pages (no HTML injection — fixture test) - [ ] deleting a page (trash) hides its comments; purge removes them ## Technical notes - data-model.md §Comments, permissions.md §Non-page objects. ## Dependencies Depends on #52. **Size**: ~1.5 days --- *Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add `de` **and** `en`), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.*
fable-5 added this to the M9 — Comments & notifications milestone 2026-07-04 14:52:49 +02:00
fable-5 added the
backend
label 2026-07-04 14:52:49 +02:00
Author
Collaborator

Implemented in 4549d6d (pipeline green, 8/8; migration 20260711220000_comments auto-applied on Test + Int).

Data modelcomments table per data-model.md: thread via parent_id (one level: replies attach to thread roots only — a reply-to-reply is rejected as comment_parent_invalid, never silently reparented), optional opaque anchor on roots, resolved_at/resolved_by, edited_at. FKs: page purge cascades the comments, root delete cascades its replies, a hard user delete nulls the author (pseudonymized accounts keep theirs).

New pond setting commentPolicy (readers default | editors) in pondSettingsSchema + updatePondInputSchema — rides the existing pond PATCH; the UI toggle lands with #92.

API (apps/api/src/comments/):

  • GET /pages/:id/comments?filter=all|open|resolved — threaded list; resolved threads carry collapsed: true by default plus open/resolved counts (AC).
  • POST /pages/:id/comments — root or reply; page read via the shared guard, then the write policy: readers → page read, editors → page write. 404-vs-403 semantics per #60: no read hides existence, failed policy on a readable page is an explicit 403 comments_editors_only.
  • PATCH /comments/:id — author only (comment_not_author), stamps editedAt.
  • DELETE /comments/:id — author (roots with replies refuse with 409 comment_has_replies — nobody takes other people's replies down), pond admins and Site Admins delete any thread (cascade).
  • POST|DELETE /comments/:id/resolve — roots only, for everyone who may comment.

Bodies are Markdown rendered through the shared sanitizing pipeline (docToHtml(markdownToDoc(..)) — the exact page renderer); the view carries both the raw body (for edit forms) and the inert html. Error codes localized de+en.

Acceptance criteria (all in comments.e2e.db.test.ts, 5 tests):

  • thread CRUD with the permission matrix — reader comments under the default policy, editor replies, outsider gets 404 on read and write, edit/delete ownership rules incl. the admin override.
  • resolved filterable + default-collapsedfilter=open/resolved plus the collapsed flag asserted both ways; unresolve reopens.
  • no HTML injection — fixture with <script> + <img onerror>: the html contains &lt;script&gt; and no live tags.
  • trash hides, purge removes — trashed page → list and mutations 404; page delete → zero comment rows.

Next in the track: #92 (UI), #93 (watches), #94 (notifications — comment events will hook into CommentsService).

Implemented in `4549d6d` (pipeline green, 8/8; migration `20260711220000_comments` auto-applied on Test + Int). **Data model** — `comments` table per data-model.md: thread via `parent_id` (one level: replies attach to thread roots only — a reply-to-reply is rejected as `comment_parent_invalid`, never silently reparented), optional opaque `anchor` on roots, `resolved_at`/`resolved_by`, `edited_at`. FKs: page purge **cascades** the comments, root delete cascades its replies, a hard user delete nulls the author (pseudonymized accounts keep theirs). **New pond setting `commentPolicy`** (`readers` default | `editors`) in `pondSettingsSchema` + `updatePondInputSchema` — rides the existing pond PATCH; the UI toggle lands with #92. **API** (`apps/api/src/comments/`): - `GET /pages/:id/comments?filter=all|open|resolved` — threaded list; **resolved threads carry `collapsed: true` by default** plus open/resolved counts (AC). - `POST /pages/:id/comments` — root or reply; page read via the shared guard, then the write policy: `readers` → page read, `editors` → page write. 404-vs-403 semantics per #60: no read hides existence, failed policy on a readable page is an explicit 403 `comments_editors_only`. - `PATCH /comments/:id` — author only (`comment_not_author`), stamps `editedAt`. - `DELETE /comments/:id` — author (roots with replies refuse with 409 `comment_has_replies` — nobody takes other people's replies down), pond admins and Site Admins delete any thread (cascade). - `POST|DELETE /comments/:id/resolve` — roots only, for everyone who may comment. Bodies are Markdown rendered through the **shared sanitizing pipeline** (`docToHtml(markdownToDoc(..))` — the exact page renderer); the view carries both the raw `body` (for edit forms) and the inert `html`. Error codes localized de+en. **Acceptance criteria** (all in `comments.e2e.db.test.ts`, 5 tests): - *thread CRUD with the permission matrix* — reader comments under the default policy, editor replies, outsider gets 404 on read *and* write, edit/delete ownership rules incl. the admin override. - *resolved filterable + default-collapsed* — `filter=open/resolved` plus the `collapsed` flag asserted both ways; unresolve reopens. - *no HTML injection* — fixture with `<script>` + `<img onerror>`: the html contains `&lt;script&gt;` and no live tags. - *trash hides, purge removes* — trashed page → list and mutations 404; page delete → zero comment rows. Next in the track: #92 (UI), #93 (watches), #94 (notifications — comment events will hook into `CommentsService`).
Sign in to join this conversation.
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: stwaidele/dorfteich#91
No description provided.