Add Phase 2 restore: polls with vote replay, tags, membership #559

Open
tobi wants to merge 1 commit from phase2-polls-tags-membership into native-site-restore
Owner

Stacked on #558 — Phase 2 of the importer rewrite: polls (with real vote replay, not just choice definitions), tags, and membership. Completes the importer's coverage of every entity type Exporter produces.

What's here

  • Polls: Poll.add({question, title_array, save}, site) — Poll.prototype.update creates one Choice per title_array entry in the same order they're passed, so choices are mapped back to the original choices[].id by that order, letting votes.json replay real ballots against the correct Choice afterward.
  • Tags: resolves tagged_id through the same idMap.content/idMap.images built in Phase 1, calls addTag per row — no new resolution logic needed.
  • Membership: resolves the member through the exact same idMap.accounts mechanism as every other entity's creator (a membership row's own creator_id/creator_name is the member, not an editor — see PR #558's design notes).

Bugs found by actually running it (ct 3998, see ~/.claude/plans/kind-sauteeing-goblet.md)

  • Vote.add hardcodes session.user directly (no user parameter at all) — null during the background/cron-driven restore, same underlying cause as PR #558's res.handlers.site fix. session.user isn't a writable property (it's a Java-backed SessionBean, confirmed live), so this uses session.login(user) instead of assigning to it.
  • Poll has no metadata collection wired up at all, unlike every other restored prototype — poll.setMetadata(...) throws "No metadata collection defined for prototype Poll". Exporter's addMetadata still queries the raw metadata table directly for export (so exports don't break), but there's nothing to write back on import, so this restore just doesn't call it.
  • Membership.add throws "already contained in the collection" restoring into any freshly created target site — Root's create action already adds the creating user as OWNER automatically, so the export's own membership row for that same account collides with the one that already exists. Fixed by updating the existing membership in place instead of assuming the target never has one — this also means restore-in-place and migrating into a site you already belong to both just work, not only the brand-new-empty-site case.

Verified end-to-end

Created a poll with 3 choices and cast a vote, tagged the fixture story, exported, imported into a fresh target site: poll choices and vote tally came back correct (matching choice had exactly 1 vote, others 0), the tag was restored and visible, and the pre-existing auto-created owner membership was updated in place rather than duplicated or erroring.

Known gaps

  • Same as PR #558: a failed restore partway through isn't rolled back, and only tested against H2 (via tools/antclick), not the MariaDB path production actually runs.
  • Account-level import (Importer.restoreAccount, looping these same per-entity helpers over every site in sites.json) is Phase 3, still deferred.

Test plan

  • Create a poll (3 choices) and vote, tag the fixture story, export
  • Import into a fresh empty target site; verify poll choices + vote tally, tag, and membership all restore correctly
  • Confirm restoring into an already-owned target site doesn't collide/error on the existing owner membership
  • Deploy to antville-test.de and spot-check against the real MariaDB path before merging
Stacked on #558 — Phase 2 of the importer rewrite: polls (with real vote replay, not just choice definitions), tags, and membership. Completes the importer's coverage of every entity type `Exporter` produces. ## What's here - **Polls**: `Poll.add({question, title_array, save}, site)` — `Poll.prototype.update` creates one `Choice` per `title_array` entry in the same order they're passed, so choices are mapped back to the original `choices[].id` by that order, letting `votes.json` replay real ballots against the correct `Choice` afterward. - **Tags**: resolves `tagged_id` through the same `idMap.content`/`idMap.images` built in Phase 1, calls `addTag` per row — no new resolution logic needed. - **Membership**: resolves the member through the exact same `idMap.accounts` mechanism as every other entity's creator (a membership row's own `creator_id`/`creator_name` *is* the member, not an editor — see PR #558's design notes). ## Bugs found by actually running it (ct 3998, see `~/.claude/plans/kind-sauteeing-goblet.md`) - `Vote.add` hardcodes `session.user` directly (no user parameter at all) — null during the background/cron-driven restore, same underlying cause as PR #558's `res.handlers.site` fix. `session.user` isn't a writable property (it's a Java-backed `SessionBean`, confirmed live), so this uses `session.login(user)` instead of assigning to it. - `Poll` has no metadata collection wired up at all, unlike every other restored prototype — `poll.setMetadata(...)` throws `"No metadata collection defined for prototype Poll"`. `Exporter`'s `addMetadata` still queries the raw metadata table directly for export (so exports don't break), but there's nothing to write back on import, so this restore just doesn't call it. - `Membership.add` throws `"already contained in the collection"` restoring into any freshly created target site — Root's `create` action already adds the creating user as `OWNER` automatically, so the export's own membership row for that same account collides with the one that already exists. Fixed by updating the existing membership in place instead of assuming the target never has one — this also means restore-in-place and migrating into a site you already belong to both just work, not only the brand-new-empty-site case. ## Verified end-to-end Created a poll with 3 choices and cast a vote, tagged the fixture story, exported, imported into a fresh target site: poll choices and vote tally came back correct (matching choice had exactly 1 vote, others 0), the tag was restored and visible, and the pre-existing auto-created owner membership was updated in place rather than duplicated or erroring. ## Known gaps - Same as PR #558: a failed restore partway through isn't rolled back, and only tested against H2 (via `tools/antclick`), not the MariaDB path production actually runs. - Account-level import (`Importer.restoreAccount`, looping these same per-entity helpers over every site in `sites.json`) is Phase 3, still deferred. ## Test plan - [x] Create a poll (3 choices) and vote, tag the fixture story, export - [x] Import into a fresh empty target site; verify poll choices + vote tally, tag, and membership all restore correctly - [x] Confirm restoring into an already-owned target site doesn't collide/error on the existing owner membership - [ ] Deploy to antville-test.de and spot-check against the real MariaDB path before merging
tobi self-assigned this 2026-07-18 09:13:30 +00:00
Poll.add({question, title_array, save}, site) restores the poll and its
choices (Poll.prototype.update creates one Choice per title_array entry,
in order — mapped back to the original choices[].id by that same order
so votes.json can be replayed against the right Choice). Tags resolve
tagged_id through idMap.content/idMap.images, same pattern as everything
else. Membership resolves the member through the shared idMap.accounts
mechanism (a membership row's own creator_id/creator_name is the member,
not an editor) rather than any separate matching logic.

All three found real bugs once actually run against the disposable test
instance (ct 3998, see ~/.claude/plans/kind-sauteeing-goblet.md):

- Vote.add hardcodes session.user directly (no user param at all) to set
  creator/creator_name — null during a background/cron restore, same
  underlying cause as the res.handlers.site fix in the previous commit.
  session.user isn't a writable property (it's a Java-backed
  SessionBean), so log the importing user in via session.login(user)
  instead of assigning to it.
- Poll has no metadata collection wired up at all (unlike every other
  restored prototype) — poll.setMetadata throws. Exporter's addMetadata
  still queries the raw metadata table directly for export, but there's
  nothing to write back on import, so this restore just doesn't call it.
- Membership.add throws "already contained in the collection" restoring
  into any freshly created target site: Root's create action already
  adds the creating user as OWNER automatically, so the export's own
  membership row for that same account collides. Update the existing
  membership in place instead of assuming the target never has one.

Verified end-to-end: poll choices and vote tally correct, tag restored
and visible, and the pre-existing owner membership correctly updated in
place rather than duplicated or erroring.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177qdRkssL6kBk5ngMDHwnP
tobi force-pushed phase2-polls-tags-membership from 9eaaa23aa9 to eac56e7447 2026-07-18 17:26:22 +00:00 Compare
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin phase2-polls-tags-membership:phase2-polls-tags-membership
git switch phase2-polls-tags-membership

Merge

Merge the changes and update on Forgejo.
git switch native-site-restore
git merge --no-ff phase2-polls-tags-membership
git switch phase2-polls-tags-membership
git rebase native-site-restore
git switch native-site-restore
git merge --ff-only phase2-polls-tags-membership
git switch phase2-polls-tags-membership
git rebase native-site-restore
git switch native-site-restore
git merge --no-ff phase2-polls-tags-membership
git switch native-site-restore
git merge --squash phase2-polls-tags-membership
git switch native-site-restore
git merge --ff-only phase2-polls-tags-membership
git switch native-site-restore
git merge phase2-polls-tags-membership
git push origin native-site-restore
Sign in to join this conversation.
No description provided.