fix(cms): homepage swap + delete in one save no longer fails publish#37
Merged
Conversation
…aps, revivals
Swapping the homepage and deleting the old one in a single save failed
('duplicate slug: Duplicate page slug "/index"'), wedging the editor in a
Draft-save-failed / Retry-publish loop. Two layers were broken:
- validation counted rows the SAME request reaps as slug owners, and
- the reconcile transactions wrote changed rows BEFORE soft-deleting
dropped ones, so even valid batches could hit
data_rows_table_slug_active_idx mid-transaction.
All three roster endpoints (/pages, /components, /layouts) now write through
one shared transaction, reconcileDataRowRoster:
1. soft-deletes run FIRST, freeing the slugs of dropped rows;
2. slug-changing writes are two-phase — park on the placeholder slug ''
(exempt from the unique index), then take the final slug — so
within-batch swaps and rotations never transiently collide;
3. a write whose id matches a soft-deleted row REVIVES it instead of
inserting into its own primary key (undo of a delete re-submits the
original id; previously a permanent 500 save loop).
Pages slug validation now ignores rows the same request reaps. VC and
layout name uniqueness is judged on the derived storage slug
(vcSlugFromName / layoutSlugFromName moved into their domain modules), so
"Button" vs "button" rejects with a readable 400 instead of dying on the
DB index as an opaque 500 — in the editor dialogs and on the server alike.
Reproduced and verified end-to-end in the live editor (swap homepage,
delete old page, save + publish in one batch; / serves the new homepage).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
The bug
Set a new page as the homepage, delete the old homepage, hit Publish → the draft save fails with
duplicate slug: Duplicate page slug "/index"and the editor wedges in a Draft save failed / Retry publish loop. (Reported with screenshots; reproduced live in the e2e environment.)The homepage is the page with slug
index. When the swap and the delete land in one save batch,PUT /admin/api/cms/pagesrejected it because slug-uniqueness validation counted the old homepage row — which this very request was about to soft-delete — as the owner ofindex.Two more failures hid behind that one:
UPDATE … slug='index'would hitdata_rows_table_slug_active_idxmid-transaction. Within-batch slug swaps/rotations (A↔B) could never succeed under per-statement unique enforcement, in any write order.Buttonvsbutton,Hero!vsHero?) passed name-only validation and died on the DB index as an opaque 500, and re-submitting a reaped row id (delete → save → undo → save) hit the soft-deleted row's primary key — a permanent 500 save loop.The fix
All three roster endpoints (
/pages,/components,/layouts) now write through one shared transaction,reconcileDataRowRoster(server/repositories/data/rows/reconcile.ts):''(exempt from the partial unique index on both dialects), then take their final slug once every old slug is free. Swaps and three-way rotations work in one batch.deleted_at) instead of inserting into its own primary key.Pages slug validation now ignores rows the same request reaps (
rowsToReap, moved from the pages handler into the shared module). VC and layout name uniqueness is judged on the derived storage slug (vcSlugFromName/layoutSlugFromName, moved into@core/visualComponents/@core/layouts), so colliding names reject with a readable 400 — in the editor dialogs and on the server alike.Verification
pagesPartialSave.test.tshandler-level via the real capability harness,reconcile.test.tsagainst the real SQLite unique index)./serves the new homepage.bun test(5354 pass / 0 fail),bun run build,bun run lintall green.🤖 Generated with Claude Code