Skip to content

Attachments per version: a removal drops from the next version only (#7) - #80

Merged
vgururao merged 2 commits into
blygger:mainfrom
aneeshsathe:feat/per-version-media
Oct 20, 2026
Merged

vgururao merged 2 commits into
blygger:mainfrom
aneeshsathe:feat/per-version-media

Conversation

@aneeshsathe

Copy link
Copy Markdown
Contributor

The second half of #7: attachments per version, as you suggested once the guard was in. It's stacked on #75 (migration 0028), so this PR's diff includes #75's commit until that merges.

The gap. Every row attached to an item was listed on every version. The live document, the feed, the cards and the item pages all read media WHERE item_id = ?. So an attachment could not leave a later version without leaving the live one too. It also led to a §5.4 bug. An attachment that's appended below the post, rather than placed in the text, never appears in content_html. DELETE /api/media/{id} checked only content_html, so it deleted the bytes of an attachment a published version still listed, and that URL then 404'd.

The change.

  • Migration 0031_version_media.sql adds version_media(item_id, version, media_id), written once at publish and never changed, like the version itself. It joins media by id, so a row detached since still lists on the versions that showed it.
    • Backfill: the latest published version keeps every attachment it lists today, so no live document changes at upgrade. An earlier version gets only what existed when it was published: appended attachments, or inline ones its text places. The dates are compared with julianday(). INSERT OR IGNORE makes the backfill safe to rerun.
    • 0030 is taken by another branch in flight (a change classifier).
  • Publish records the version's attachments in the same batch as the version. It uses the same rule as before (visibleMedia: appended, or placed in the text), and only rows still attached at the write, so a removal racing a publish wins.
  • Every public surface reads the version's own list: buildItemJson, the RSS description, the feed cards (loadFeedData) and the fragment and thread pages, including og:image.
  • DELETE /api/media/{id} now means "drop from the next version". It detaches the row when any version lists or shows it, and marks the item dirty when the live version lists it. The bytes are deleted only when nothing published references them.
  • Public bytes: /media/:file also serves anything a version lists.
  • GET /api/items/{id} gains published_media, what the live version lists. It's additive and stays within the one detail query.
  • Editor: a working attachment the live version lists offers "drop from the next version" instead of "remove". Dropped ones are shown greyed below, with "in the live version, leaves with the next one".

One behaviour change to flag. An attachment uploaded to a published item now appears with that item's next version, not at once. The upload marks the item dirty so the Studio shows unpublished changes. This follows §5.4, where a new attachment is part of a new version. But a client that uploaded to a live item and expected it to show immediately will now need to publish. Three existing tests did exactly that and now publish after attaching.

Not in this PR. There is no undo for a drop: re-attaching needs a route, and I didn't want to add one without asking. Restoring an old version's text doesn't bring back attachments dropped since.

Tests.

  • New test/version-media.test.ts:
    • drop affects the next version only, and the bytes stay served;
    • the page and feed follow the new version;
    • an upload to a published item waits for the next version;
    • an attachment no version listed is deleted outright;
    • inline attachments follow their version's text;
    • the backfill keeps live documents and limits earlier versions to what existed then.
  • New e2e test in e2e/studio.spec.ts: drop an appended attachment from a published item in the editor, and see it listed as leaving.
  • Updated: public-page-performance fixtures add version_media rows, and media/pages tests publish after attaching.

Results. Typecheck clean, and sdk:generate is stable.

  • Worker suite: 1754 passed, 5 skipped, 0 failed (143 files).
  • UI: 97/97.
  • e2e attachment tests: 4/4, desktop and mobile.

An earlier full run on a heavily loaded machine timed out three unrelated tests at the 5 s limit. They pass alone, take about the same time on #75's branch, and passed in the clean rerun.

Refs #7. Depends on #75.

🤖 Generated with Claude Code

aneeshsathe and others added 2 commits October 18, 2026 13:39
…blygger#7)

POST /api/media with an item_id hashes the bytes (SHA-256) before the R2
write. A row on that item with the same hash, MIME type and placement is
returned as 200 { id, url, mime, duplicate: true } and nothing is stored.
Migration 0028 adds a nullable media.sha256 (no backfill) with an
(item_id, sha256) index, and recreates change_media_update in the
remote-safe shape with the new column watched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lygger#7)

Migration 0031 adds version_media(item_id, version, media_id), written at
publish with the version and never changed. The item document, RSS, feed
cards and item pages read the version's own list, so a later version can go
without an attachment while the earlier ones keep listing and serving it
(§5.4). The backfill keeps every live document as it was.

DELETE /api/media/{id} now detaches anything a version lists, not only what
a version's text shows: an appended attachment's bytes were deleted while
its version still listed them. An upload to a published item is listed from
the next version on and marks the item dirty. GET /api/items/{id} gains
published_media, and the editor offers "drop from the next version".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vgururao
vgururao merged commit 1c44ac8 into blygger:main Oct 20, 2026
7 of 9 checks passed
vgururao added a commit that referenced this pull request Oct 20, 2026
…awal mutants cover version_media

Same-second uploads tie on media.created; through the version_media join the
tie fell back to random media ids, so #80's first test failed in CI. rowid
breaks the tie in every attachment list. #80's version_media clause is a new
route keeping published bytes after withdrawal, so both withdrawal mutants now
restrict it as well (both caught).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BqkoZVrageDUQrG94Km4C
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants