Skip to content

MM-69268 - Page tree CRUD + URL API: spaces, pages, move, duplicate - #4

Merged
catalintomai merged 67 commits into
masterfrom
MM-69268-page-tree-crud-url-api-v2
Jul 16, 2026
Merged

MM-69268 - Page tree CRUD + URL API: spaces, pages, move, duplicate#4
catalintomai merged 67 commits into
masterfrom
MM-69268-page-tree-crud-url-api-v2

Conversation

@catalintomai

@catalintomai catalintomai commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the page-tree CRUD + URL API surface under /api/v1: space CRUD and membership, page CRUD scoped to a space, tree operations (move/reorder, duplicate, move-to-space), WebSocket events, and pagination.

  • Space CRUD: create (stands up a ChannelTypeSpace backing channel), get, patch, delete/restore (archives/un-archives the backing channel); team space listing plus space member add/remove/list.
  • Page CRUD: operations scoped to {space_id, page_id}, closing the race where a page is relocated out of its space mid-request; children listing.
  • Move + reorder: MovePage reparents/repositions within a space, rejecting cycles and depth-cap breaches; optimistic-concurrency check via expected UpdateAt with force override.
  • Duplicate: copies a page (optionally its whole subtree) in place or into another page/space; new Pages.OriginalId index migration.
  • Move-to-space: relocates a page and its subtree across spaces within the same team.
  • WebSocket events for every page/space mutation, broadcast to the backing channel (member events to the affected user).
  • Paginated listings with clamped page/per-page params; i18n-backed AppError messages; every route gated on the EnableDocs feature flag.
  • Dependency bump (go.mod/go.sum) to pin the core branch providing ChannelTypeSpace.

Supersedes #3.

Core PR:

mattermost/mattermost#37321

Ticket Link

https://mattermost.atlassian.net/browse/MM-69268

catalintomai and others added 30 commits June 23, 2026 12:46
Drop the hello-world scaffolding inherited from the plugin starter
template: the /hello API route and handler, the hello slash command and
its mocks, the demo background job, the KV store sample, public/hello.html,
and the placeholder webapp test. The router, auth middleware, and
configuration plumbing the Docs feature builds on are kept.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b0bc1baa-243e-471a-a2c1-7aa568d5a0a2

📥 Commits

Reviewing files that changed from the base of the PR and between 7526876 and 5f724e1.

📒 Files selected for processing (9)
  • assets/i18n/en.json
  • server/app/page.go
  • server/app/space.go
  • server/app/ws_events_test.go
  • server/store/page_hierarchy.go
  • server/store/page_move_test.go
  • server/store/page_store.go
  • server/store/space_store.go
  • server/store/store_test.go
💤 Files with no reviewable changes (1)
  • assets/i18n/en.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • server/app/ws_events_test.go
  • server/store/page_hierarchy.go
  • server/store/page_move_test.go
  • server/store/space_store.go
  • server/app/space.go

📝 Walkthrough

Walkthrough

This PR adds Mattermost Docs APIs for spaces and pages, including membership checks, CRUD, pagination, hierarchy movement, duplication, optimistic locking, WebSocket events, feature-flag gating, persistence safeguards, and extensive integration tests.

Changes

Spaces and pages API

Layer / File(s) Summary
API routing and request handling
server/api.go, server/api_page.go, server/api_space.go
Registers space and page routes with authentication, Docs feature gating, membership enforcement, bounded JSON decoding, standardized responses, and pagination.
Service and persistence workflows
server/app/*, server/store/*
Adds space lifecycle and membership operations, scoped page mutations, duplication, reparenting, cross-space moves, depth/cycle checks, optimistic locking, and transactional persistence.
Models, events, and configuration
server/model/*, server/app/ws_events.go, server/configuration.go, server/plugin.go
Adds summary projections, validation updates, WebSocket event publication, cached Docs feature flags, and updated plugin wiring.
Validation and integration coverage
server/*_test.go, server/app/*_test.go, server/store/*_test.go
Adds end-to-end, service, store, pagination, hierarchy, membership, locking, event, and request-decoding tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: page-tree CRUD and URL API work for spaces, pages, move, and duplicate.
Description check ✅ Passed The description accurately covers the PR's main scope, including space/page CRUD, tree moves, duplication, events, pagination, and feature gating.
Docstring Coverage ✅ Passed Docstring coverage is 86.60% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch MM-69268-page-tree-crud-url-api-v2

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/store/page_hierarchy.go (1)

66-98: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Exclude version snapshots from the descendants CTE.

Unlike the new ancestry and subtree templates, this CTE traverses rows where OriginalId != ''. Snapshots can consequently appear in page trees, consume the descendant limit, and be treated as duplicateable descendants.

Proposed fix
-			FROM DOCS_Page WHERE Id = $1 AND DeleteAt = 0
+			FROM DOCS_Page WHERE Id = $1 AND DeleteAt = 0 AND OriginalId = ''
...
-			WHERE p.DeleteAt = 0 AND d.depth < %d
+			WHERE p.DeleteAt = 0 AND p.OriginalId = '' AND d.depth < %d
...
-	WHERE d.Id != $1 AND p.DeleteAt = 0
+	WHERE d.Id != $1 AND p.DeleteAt = 0 AND p.OriginalId = ''
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/store/page_hierarchy.go` around lines 66 - 98, Update
computeDescendantsCTE to exclude version snapshot rows by requiring OriginalId =
'' in both the recursive page traversal and the root seed, matching the ancestry
and subtree CTE behavior. Ensure snapshots cannot enter the descendants result
or consume the hierarchy-depth limit.
🧹 Nitpick comments (3)
go.mod (1)

3-18: 🧹 Nitpick | 🔵 Trivial

Track the temporary core-branch pin to avoid silent breakage.

Pinning server/public to the head of an unreleased core branch (rather than a tag) means a rebase/force-push on that branch invalidates the pseudo-version+commit-hash pair, breaking go mod/go.sum resolution for anyone re-fetching. The comment already documents this as temporary, which is good — consider also tracking it with a CI check or ticket link so it isn't forgotten once the core branch ships a release tag.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go.mod` around lines 3 - 18, Track the temporary server/public pseudo-version
pin in an automated CI check or an existing issue/ticket reference, using the
dependency declaration and its accompanying temporary-pin comment as the
integration points. Ensure the tracking mechanism detects or reminds maintainers
to replace the unreleased core-branch pin with a release tag once the
space-channel changes ship.
server/app/ws_events_test.go (2)

139-152: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert the total number of cross-space broadcasts.

AssertCalled permits an additional broad or full-payload event, so this privacy contract could regress without failing. Require exactly two publications.

Proposed test hardening
 mockAPI.AssertCalled(t, "PublishWebSocketEvent", "page_moved_to_space",
     map[string]any{
         "page_id":         moved.Id,
         "target_space_id": spaceB.Id,
         "new_parent_id":   "",
     },
     &mmmodel.WebsocketBroadcast{ChannelId: chB})
+mockAPI.AssertNumberOfCalls(t, "PublishWebSocketEvent", 2)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/ws_events_test.go` around lines 139 - 152, Strengthen the
assertions in the cross-space move test around the two PublishWebSocketEvent
calls to require exactly two total publications, while preserving the existing
source-space and target-space payload checks. Use the mockAPI call-count
assertion for PublishWebSocketEvent so extra broad or full-payload broadcasts
fail the test.

239-258: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Pin the fallback publication before channel archival.

The test checks both calls occurred but not their order. A regression that publishes after DeleteChannel would still pass, although an archived-channel broadcast reaches nobody. Add an ordering assertion or sequence recorder.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/ws_events_test.go` around lines 239 - 258, Update
TestServiceDeleteSpace_SnapshotFailureFallsBackToChannelBroadcast to record the
relative order of PublishWebSocketEvent and DeleteChannel, then assert the
fallback publication occurs before channel archival. Keep the existing argument
assertions and failure-path setup unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/app/page.go`:
- Around line 182-215: Update Service.RestorePage and the underlying
store.RestorePage operation to return the restored page row directly from the
atomic transaction. Remove the readBackAfterRestore/GetPageInSpace call and use
the returned page for publishing and the method result, while preserving
existing error mapping and validation.

In `@server/store/space_store.go`:
- Around line 368-417: Update withSpaceMembershipLock so any advisory unlock
failure marks the database connection for discard rather than returning it to
the pool. Track unlock errors from both the best-effort unlock after acquisition
failure and the deferred unlock after fn, then close or discard the connection
through the pool’s supported mechanism while preserving the existing warning
logs and error behavior.

In `@server/store/store_test.go`:
- Around line 906-911: Update the cycle-handling test around
FetchDescendantRowsForTest to assert that descendants is empty, rather than
discarding it with _ = descendants. Keep the existing no-error assertion and
verify the cyclic root is excluded, including preventing any returned root or
duplicate rows.

---

Outside diff comments:
In `@server/store/page_hierarchy.go`:
- Around line 66-98: Update computeDescendantsCTE to exclude version snapshot
rows by requiring OriginalId = '' in both the recursive page traversal and the
root seed, matching the ancestry and subtree CTE behavior. Ensure snapshots
cannot enter the descendants result or consume the hierarchy-depth limit.

---

Nitpick comments:
In `@go.mod`:
- Around line 3-18: Track the temporary server/public pseudo-version pin in an
automated CI check or an existing issue/ticket reference, using the dependency
declaration and its accompanying temporary-pin comment as the integration
points. Ensure the tracking mechanism detects or reminds maintainers to replace
the unreleased core-branch pin with a release tag once the space-channel changes
ship.

In `@server/app/ws_events_test.go`:
- Around line 139-152: Strengthen the assertions in the cross-space move test
around the two PublishWebSocketEvent calls to require exactly two total
publications, while preserving the existing source-space and target-space
payload checks. Use the mockAPI call-count assertion for PublishWebSocketEvent
so extra broad or full-payload broadcasts fail the test.
- Around line 239-258: Update
TestServiceDeleteSpace_SnapshotFailureFallsBackToChannelBroadcast to record the
relative order of PublishWebSocketEvent and DeleteChannel, then assert the
fallback publication occurs before channel archival. Keep the existing argument
assertions and failure-path setup unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 78af1742-8c39-4596-bcb0-3574e422e8e0

📥 Commits

Reviewing files that changed from the base of the PR and between 2d8b0b6 and 69942cd.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (46)
  • .github/workflows/ci.yml
  • README.md
  • assets/i18n/en.json
  • go.mod
  • plugin.json
  • server/api.go
  • server/api_handler_test.go
  • server/api_page.go
  • server/api_params_test.go
  • server/api_space.go
  • server/app/page.go
  • server/app/page_duplicate_test.go
  • server/app/page_hierarchy.go
  • server/app/page_move_test.go
  • server/app/page_move_to_space_test.go
  • server/app/page_reorder_test.go
  • server/app/pagination.go
  • server/app/pagination_test.go
  • server/app/service.go
  • server/app/service_test.go
  • server/app/space.go
  • server/app/space_test.go
  • server/app/ws_events.go
  • server/app/ws_events_test.go
  • server/configuration.go
  • server/internal/testutil/fixtures.go
  • server/model/draft.go
  • server/model/draft_test.go
  • server/model/page.go
  • server/model/page_test.go
  • server/model/props.go
  • server/model/space.go
  • server/model/space_test.go
  • server/plugin.go
  • server/store/draft_store.go
  • server/store/export_test.go
  • server/store/migrations/000004_add_page_originalid_index.down.sql
  • server/store/migrations/000004_add_page_originalid_index.up.sql
  • server/store/page_duplicate.go
  • server/store/page_hierarchy.go
  • server/store/page_move.go
  • server/store/page_move_test.go
  • server/store/page_store.go
  • server/store/space_store.go
  • server/store/store.go
  • server/store/store_test.go

Comment thread server/app/page.go
Comment thread server/store/space_store.go
Comment thread server/store/store_test.go Outdated
@catalintomai
catalintomai requested a review from edgarbellot July 13, 2026 14:25

@jgheithcock jgheithcock left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went through this fairly thoroughly with AI and the consensus was LGTM!

@JulienTant JulienTant left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, I actually didn't know about github.com/mattermost/mattermost/server/public/plugin/plugintest and now I realize maybe making you extract logger last time was not necessary if you can abstract any call to the plugin api... oops.

Thanks!

@catalintomai
catalintomai merged commit 598cb9c into master Jul 16, 2026
6 checks passed

@edgarbellot edgarbellot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@catalintomai LGTM, I just have one request.

Comment thread server/app/page.go
}

s.logDebug("Creating page", "space_id", spaceID, "parent_id", parentID, "user_id", userID)
s.log.Debug("Creating page", "space_id", spaceID, "parent_id", parentID, "user_id", userID)

@edgarbellot edgarbellot Jul 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The models implement Auditable - can we wire up audit records for the mutating operations? Right now they rely on s.log.Debug calls.

The Playbooks plugin has a good example of the pattern here:

auditRec := plugin.MakeAuditRecord("createPage", mmmodel.AuditStatusFail)
defer s.api.LogAuditRec(auditRec)
mmmodel.AddEventParameterToAuditRec(auditRec, "userID", userID)
mmmodel.AddEventParameterAuditableToAuditRec(auditRec, "page", *page)
// ... do the work ...
auditRec.Success()

Having this in place would make incident response much easier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2: Dev Review Requires review by a core committer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants