Skip to content

fix: actionable slug-conflict error when publishing a staged slug#2036

Open
swissky wants to merge 2 commits into
emdash-cms:mainfrom
swissky:fix/publish-slug-conflict
Open

fix: actionable slug-conflict error when publishing a staged slug#2036
swissky wants to merge 2 commits into
emdash-cms:mainfrom
swissky:fix/publish-slug-conflict

Conversation

@swissky

@swissky swissky commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Publishing an entry whose staged slug (stored as _slug in a draft revision) collides with another entry's (slug, locale) failed with an opaque 500 and a raw D1/SQLite message (D1_ERROR: UNIQUE constraint failed: ec_news.slug, ec_news.locale) — after draft data had already been synced into the live columns. The staged slug lands on the live column only inside repo.publish(), bypassing the uniqueness handling that guards direct slug edits.

This PR:

  • Validates the staged slug inside publish() before any write, against all rows in the entry's locale — including drafts and trashed entries, since the (slug, locale) unique index covers them. On conflict it throws a validation error naming the slug and the conflicting entry id, which the handler maps to the same SLUG_CONFLICT (409) as direct slug edits in create/update.
  • Reorders the publish writes so the slug UPDATE (the only statement that can hit the constraint) runs before syncDataColumns — if a concurrent write takes the slug in the race window, no partial publish is left behind on D1 (which has no transactions to roll back).
  • Maps the unique-constraint fingerprint in handleContentPublish's catch to SLUG_CONFLICT as a backstop for that race window, matching the existing create/update handling — a raw SQLite error never reaches the client.
  • Skips the check for rows with a NULL locale (unique indexes treat NULLs as distinct, so those rows can't collide).

Closes #2034

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Cursor + Fable 5

Screenshots / test output

Regression tests: staged slug conflicting with a published entry, with a trashed entry, republish with own slug, same slug across locales, and the handler-level SLUG_CONFLICT mapping.

Test Files  239 passed (239)
     Tests  3190 passed (3190)

swissky added 2 commits July 14, 2026 14:37
A slug edit staged as `_slug` in a draft revision lands on the live slug
column only inside repo.publish(), bypassing the uniqueness handling that
guards direct slug edits. When the staged slug collided with another
entry's (slug, locale), the UNIQUE constraint fired mid-publish and
surfaced as an opaque 500 with a raw D1/SQLite message — after draft data
had already been synced into the live columns.

publish() now validates the staged slug before any write (against all
rows, including drafts and trashed entries, since the unique index covers
them) and throws an EmDashValidationError naming the conflicting slug and
entry. handleContentPublish additionally maps a unique-constraint
fingerprint to SLUG_CONFLICT as a backstop for the race window, matching
create/update.

Fixes emdash-cms#2034
Adversarial-review follow-ups: tag the pre-check error so publish maps it
to the same 409 SLUG_CONFLICT as create/update (not a generic 400), write
the slug before syncing data columns so a constraint hit in the race
window leaves no partial publish on D1, and skip the check for NULL
locales (unique indexes treat NULLs as distinct).
@changeset-bot

changeset-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5a53e1c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jul 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2036

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2036

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2036

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2036

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2036

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2036

emdash

npm i https://pkg.pr.new/emdash@2036

create-emdash

npm i https://pkg.pr.new/create-emdash@2036

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2036

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2036

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2036

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2036

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2036

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2036

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2036

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2036

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2036

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2036

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2036

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2036

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2036

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2036

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2036

commit: 5a53e1c

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

Labels

area/core bot:review Trigger an emdashbot code review on this PR cla: signed review/needs-review No maintainer or bot review yet size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Publish fails with opaque D1 UNIQUE error when staged slug collides with an existing published entry

1 participant