Repository navigation
Conversation
TreeService.SubtreeRoutes returns a node followed by its descendants,
depth-first in navigation order, as RouteEntry values computed under the
read lock without file reads. Assembling a subtree from several ChildRoutes
calls would race with moves between the calls. PageRoutes and SubtreeRoutes
share one walk.
RouteEntry gets ParentID ("root" for the top level), so a caller can nest
the flat list without walking PageNode.Parent outside the lock. The tree
root is not a subtree here: PageRoutes covers the whole tree.
Refs perber#1252
TreeReader.Tree(ctx, scope) returns the scope as a tree: the scope root on top, nothing above or beside it, children in navigation order. A subtree scope holds every page below the root as the tree is now, so pages created or moved in appear and pages moved out disappear. A single-page scope is the root alone with no children, so its subpages are not revealed. It reads TreeService.SubtreeRoutes, one locked walk, and nests the flat list by ParentID. TreeNodeView is an allow-list like PageView (id, title, kind, children): no slug or path, which would reveal the tree above the scope, and no position, pin state, authors or timestamps. An unknown or deleted root is ErrNotFound; operational errors pass through. ADR-0015 records TreeNodeView's allow-list and SubtreeRoutes as a second structural query on the tree. Refs perber#1252
GET /api/shared/{token}/tree returns the shared page as the top of a
scopedread.TreeNodeView tree. A link that includes subpages holds every
page below it as the tree is now, so pages created or moved in appear and
pages moved out disappear; a single-page link returns the page alone with
no children. Nothing above or beside the shared page is included.
The route sits in the same capability group as the page routes and answers
the same way: one 404 body for every failure, 503 during a restore, a
generic 500 otherwise. Its failed lookups count towards the same
per-client limit as the page routes'.
With a second use case, resolving the token to a scope and mapping
scoped-read errors move into resolveScope and scopedReadError, and the
handlers share serveShared.
Refs perber#1252
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue
Part of #1252 (PR 4 of the agreed series; does not close it).
What changed
The navigation for a share link, as you outlined PR 4: root-only vs subtree, no ancestors above the root, moved and new descendants.
Commit 1:
feat(tree),TreeService.SubtreeRoutes(id)RouteEntryvalues from one walk under the read lock, without file reads. Building the subtree from severalChildRoutescalls would race with a move between the calls.PageRoutesandSubtreeRoutesshare the walk.RouteEntrygetsParentID("root"for the top level), so a caller can nest the flat list without walkingPageNode.Parentoutside the lock.RouteEntryis never serialised as it is; both existing consumers map it explicitly.ErrPageNotFound);PageRoutescovers the whole tree. LikeIsInSubtreein PR 1, it is a question about structure, not visibility.Commit 2:
feat(scopedread),TreeReader.Tree(ctx, scope)(the shape from your proposal, ADR-0015)children, so the client has one code path and the subpages are not revealed.TreeNodeViewis an allow-list likePageView:id,title,kind,children(nevernull). No slug or path (they would reveal the tree above the scope), no position, pin state, authors or timestamps.TestTreeNodeView_FieldsAreAllowListedguards it.ErrNotFound; operational errors pass through, with the same "never send the text to a scoped caller" rule.Commit 3:
feat(shares),GET /api/shared/{token}/tree/treeand on the page routes count towards the same per-client limit, so a second route doesn't double the budget.resolveScopeandscopedReadError, and the handlers shareserveShared.docs/apiregenerated (additions only),scopedread.TreeNodeViewindocumentedModels.Size: 268 lines of production code. ADR-0015 is updated:
TreeNodeView's allow-list in decision 5,SubtreeRoutesnext toIsInSubtreein decision 8.Decisions worth a look:
/pages/{id}exists). If the UI in PR 7 wants readable relative paths, a relative slug can be added without breaking the shape./api/tree. A link on a very large section returns the whole section. A valid token is needed, nothing is read from disk, and the read lock is held only for the in-memory walk; nesting and encoding happen outside it. If this ever matters, a short per-link cache or a concurrency cap on/sharedwould be the place.SubtreeRouteswould avoid it if needed.Noted in review, not changed here (both already apply to the page routes from PR 3):
Not in this PR: scoped assets and link safety (PR 5), the remaining items of PR 6, the UI (PR 7).
How I tested it
internal/core/tree:SubtreeRoutesorder (afterSortPages), parent IDs, errors (not loaded, unknown, empty, tree root);internal/scopedread:SortPages;ErrNotFound.internal/wiki/shares: the grant → scope mapping for both link kinds, the error mapping, the exact response body, and a 500 that never carries the cause.internal/http(real router):/pageand/treeshare one limit;/treeshapes (trailing slash, token in the query, HEAD, POST).golangci-lint: no new findings (only the existing SA4023 ininternal/wiki/restore/routes.go).go test -raceis unsupported on Windows/arm64. The concurrency tests are written for-racein CI.Checklist
if featurebranches in shared code (rule 1)