MM-70092: Preserve drafts when a page is soft-deleted - #14
Open
catalintomai wants to merge 2 commits into
Open
Conversation
📝 WalkthroughWalkthroughPage deletion no longer hard-deletes drafts. Liveness filtering hides drafts during soft deletion. Restoration reveals eligible drafts and preserves their fields. Publishing can detect stale baselines or force publication. ChangesDraft lifecycle preservation
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
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.
Fixes MM-70092.
Problem
Deleting a page soft-deletes the page (recoverable via
RestorePage) but hard-deleted every user's drafts on that page as a side effect.deleteDraftsForPagescoped itsDELETEbyPageIdandSpaceId— but not by owner — so any space member with delete rights permanently destroyed other collaborators' unpublished work, with no confirmation and no recovery even after restoring the page.This also made
DeletePageinconsistent withDeleteSpace, which already left drafts untouched precisely because that soft-delete is reversible.Fix
Remove
deleteDraftsForPageentirely and let the pre-existing read-sideapplyDraftLivenessFilterhide drafts whose page is deleted. A page delete's blast radius now matches its reversibility: drafts are hidden while the page is deleted and reappear intact on restore.The production change is a net deletion. No schema change, no migration, no new filter — the read path already had the exact predicate needed (
p.DeleteAt = 0 AND p.OriginalId = '' AND p.SpaceId = d.SpaceId), applied by all five draft read paths.reparentDraftsForPageis unchanged and still runs: a pending child draft (ParentId= the deleted page) is still reparented to the deleted page's parent rather than left dangling. The two cascades are disjoint — one matches onParentId, the other onPageId.Behavior changes
DeletePageandRestorePageeach bump the page'sEditAt, so the draft's write-onceBaseEditAtis two bumps stale. A non-force publish returns 409app.page_draft.publish.edit_conflict.app_error;force=truepublishes.MaxDraftsPerUserPerSpace. This is deliberate — it bounds total row growth — but see follow-ups.Testing
Full
./server/...suite passes. New and updated coverage:TestDeletePage(store) — drafts hidden across two users while deleted, then field-for-field identical after restore, includingUpdateAt(the CAS tokenDeleteDraftVersionanddeletePublishedDraftTxkey off).TestDeletePagePreservesEditDraftAndReparentsChildDraft— both cascades in one delete, verified isolated.TestUpsertDraftQuotaCountsDraftsHiddenByPageDelete— pins the quota consequence above.TestRestoreSpaceLeavesIndividuallyDeletedPageDraftHidden—RestoreSpace's stamp-scoped un-cascade carried through to draft visibility.TestDiscardPathsReachDraftHiddenByPageDelete— the discard paths are not liveness-gated, so a hidden draft is still discardable.TestPublishAfterPageDeleteRestoreConflictsThenForceSucceeds(app) — the 409-then-force sequence above.TestDraft— a draft on a live page hidden by its space's soft-delete (exercises the space-liveness JOIN rather than thep.Id IS NULLbranch).🤖 Generated with Claude Code