Page hierarchy: schema, parentId in page API, reparent via position endpoint #106
Labels
No Label
area:auth
area:docs
area:export
area:ops
area:storage
area:supply-chain
auth
backend
blocked
collab
deployment
docs
effort:L
effort:M
effort:S
frontend
plugins
qa
vs-nfd
vs-nfd:blocker
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: stwaidele/dorfteich#106
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Pages become a tree: every page can have child pages (Confluence-style), mirroring the existing label hierarchy (
Label.parentId, self-relation, max depth 6). Slugs and all URLs stay flat and pond-unique (@@unique([pondId, slug])unchanged) —parentIdis pure organization, so moving a page never breaks wikilinks, public URLs, or the relativehrefs in cached public HTML.Scope
parent_idonpageswith self-relation"PageHierarchy",onDelete: SetNull, index onparent_id(apps/api/prisma/schema.prisma).packages/shared/src/pages.ts:MAX_PAGE_DEPTH = 6;parentId: string | nullonPageView/PageListItemView;createPageInputSchemagains optional nullishparentId;repositionPageInputSchemagains optionalparentId(one atomic endpoint for reorder + reparent).packages/shared/src/labels.ts(collectSubtreeIds,collectAncestorIds,labelDepth,subtreeHeight,buildLabelTree) to a{ id; parentId }element type so pages reuse them (e.g. sharedtree.ts+buildPageTree).PagesService.create(apps/api/src/pages/pages.service.ts): validateparentId— same pond, not trashed, resulting depth ≤ 6 (page_depth_exceeded).PagesService.reposition: whenparentIdis present, reparent atomically with the sort-key placement. Reject moving a page under itself or its own descendant (page_cycle, viacollectSubtreeIds) and depth violations (parent depth +subtreeHeightof the moved subtree ≤ 6).PagesService.list: includeparentId, but null it in the response when the parent is not in the caller's readable set (the readable set is already computed for filtering) — no UUID leak; the client shows such pages at root.page_cycle,page_depth_exceededin the errors i18n namespace (de+en).Acceptance criteria
parentIdpersists it; creating without stays root-level; parent from another pond / trashed parent / depth 7 are rejected with the right codes.PATCH /pages/:id/positionwithparentIdreparents + places atomically; moving a page under its own descendant returns 4xxpage_cycle.GET /ponds/:id/pagescarriesparentId; a label-restricted reader whose parent page is unreadable seesparentId: nullfor the child.pagesand reposition db test suites).Technical notes
LabelsService.movefor cycle/depth checks.onDelete: SetNullis the FK backstop; purge/trash semantics are refined in the follow-up delete-modes issue.apps/api/prisma/migrations/as usual.Dependencies
None.
Size
~1.5 days.
Implemented in
eb6b0d5.pages.parent_idself-relation (SetNull backstop, index, migration20260714000000_page_hierarchy); generic tree helpers extracted topackages/shared/src/tree.ts(labels re-export them);MAX_PAGE_DEPTH = 6;createPageInputSchema.parentId;PATCH /pages/:id/positionreparents atomically (absent = keep, null = root) with 409page_cycle/page_depth_exceeded; the list nullsparentIdwhen the caller may not read the parent. Covered byhierarchy.db.test.ts(7 tests) — create/404s/depth/cycle/atomic reparent/permission nulling all green.