Skip to content

SQL into repositories (backend campaign 11) - #100

Closed
theobong wants to merge 11 commits into
chore/backend-campaign-10-inplace-b10-b12from
chore/backend-campaign-11-sql-repositories
Closed

theobong wants to merge 11 commits into
chore/backend-campaign-10-inplace-b10-b12from
chore/backend-campaign-11-sql-repositories

Conversation

@theobong

@theobong theobong commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

What changed

Every SQL statement that ran outside a repository now lives in one, behavior-neutrally: statements in services, helpers, route files, the media worker's retention job and seams, and bare *.drizzle.ts helper modules became named repository methods (or fragments exported by the repository that owns the tables). The copies of the Postgres unique-violation check and the SQL fragment type now have one home in db/. Nothing a user sees should change.

Before you start

  • Where: staging (civfix.dev, admin.civfix.dev, the staging mobile build) after this merges to main
  • Sign in as: your own accounts, A (host) and B (attendee), with emailed codes; a throwaway account for the delete step; an operator session on admin.civfix.dev
  • Stacked on In-file cleanup: host comms, admin mail, admin reports, data export (backend campaign 10) #99 (in-file cleanup 10). Read only this PR's diff against that branch.
  • Size: 6208 counted lines. Campaign PR; the author approved PRs over the 400-line cap for this cleanup. Most of it is SQL moved verbatim; read it with git diff --color-moved=dimmed-zebra --color-moved-ws=allow-indentation-change.
  • Reading order: the first commit adds the shared helpers; then one commit per area (data export, worker, account erasure, jurisdiction and geocoding, admin, host, chat, media and reports); the last commit updates the docs.

Verify

Nothing new to verify: every step below is a regression check of a flow whose queries moved.

Regression

Deleting an account — [Web]

  1. With the throwaway account open "Settings", then "Account", and click "Delete account"
  2. Click "Email me a code", enter the code and click "Permanently delete" — Expect: you are signed out and the account's posts show as "Deleted User"

Requesting your data — [Web] [Mobile]

  1. Open "Settings", then "Account", then "Request my data" (once only) — Expect: "We emailed a copy of your data to ."
  2. Check the inbox — Expect: "Your civfix data export" with the same sections as before

Photo reports and who can see them — [Web] [Mobile]

  1. As A click "Report", add a photo, place the pin, pick a category, type a title and click "Drop pin" — Expect: "Pin dropped. We're on it."; after about 30 seconds the photo shows on the report
  2. As B open the same report — Expect: the photo shows
  3. As A send B a direct message with a photo — Expect: B sees the photo; a third account cannot open its link

Guest reports — [Web]

  1. Signed out, click "Report", add a photo, place the pin, pick a category, type a title and click "Drop pin" — Expect: "Pin dropped. We're on it."

Chat — [Web] [Mobile]

  1. A: open B's profile, click "Message", send a message — Expect: B sees it live
  2. A: reply to one of B's messages, mention B with @, and react with a like — Expect: both see the reply, the mention and the like chip
  3. A: edit a message and delete another ("Delete message") — Expect: "(edited)" and "Message removed" for both
  4. Scroll up in a long conversation — Expect: older messages load in order

Hosting — [Web] [Mobile]

  1. A: "Host dashboard", then "Make announcement", send it — Expect: "Announcement sent." and B gets it
  2. A (web): on the "Attendees" tab click "Export", then the attendees export, then "Download" when ready — Expect: the roster CSV downloads
  3. A: "Host dashboard", "Team", then "Invite"; invite B as co-host with "Send the invite" — Expect: "Invite sent"

Admin — [Admin]

  1. On the Dashboard click "Open reports", open a report and click "Send to city" — Expect: "Sent to "
  2. Click "Review accounts", open a user — Expect: the tabs load
  3. Click "Open moderation", approve an item — Expect: it leaves the queue

Findings addressed

Campaign PR 11 (SQL into repositories): BE-SQL-001, 003 to 007, 008 (reference codes), 009, 011, 013 (already fixed by #93), 014, 015 (fragment module), 016 to 018, 020 (repository method), 021 to 030, 032, 033, 034 (partly), 036, 037 (partly), 038 to 054; BE-ARCH-005 (SQL part).

Where the SQL went:

  • Account erasure: the cross-domain lane (event transfers, organization release, attendee scrub, moderation snapshots, verification documents) is erasure-repository.drizzle.ts, called inside the same erasure transaction in the same order.
  • Personal data export: one repository method per export section.
  • Worker: the six retention deletes are retention-repository.drizzle.ts (exported to the worker as @civfix/api/retention-repo); the photo duplicate lookup is a worker-repository method.
  • Media authorization and content reports: media-authorization-repository.drizzle.ts; the avatar claim and the upload lock are media-claim-repository.drizzle.ts.
  • Chat: room history and pins (room-messages-repository), read watermarks, reply targets, mentions, reactions and attachments are repositories; user search has its own repository; report chat meta moved from the route into the report chat repository.
  • Host: standing checks, registration retention, host exports, team lookups and the broadcast audience queries are repositories.
  • Admin: the audit insert, the bounce lookups, the discovery probe, system health counts and the admin event, person and category fragments live with the repositories that own the tables.
  • Jurisdiction and geocoding: a jurisdiction repository, a geocode cache repository, an affiliation repository and a reference-code repository; the readiness ping is a db/client.ts helper.

Decisions for the reviewer

  • Proof that no statement changed: the offline SQL recorder (from Backend test safety net: SQL transcripts, CSRF pairing, authz and characterization tests #92, run locally) was regenerated on In-file cleanup: host comms, admin mail, admin reports, data export (backend campaign 10) #99's branch: 860 cases. After the move all 860 are byte-identical: 842 under the same key, 13 under the key of the repository they moved to, and the 5 media-authorization entry points (which now take the repository instead of the handle) produce identical transcripts when run through the new repository. 96 new cases cover the new repository methods.
  • Same handle everywhere: statements that ran on a caller's transaction take it as their first parameter; the membership check stays on the Drizzle pool it used; repositories are built at the point the old code already held the handle, and building one does no I/O.
  • Four identical statements are now shared by the media-authorization and content-report lanes (direct message participant, event member, group access, report access). Their text was identical; a future change to one lane now changes both.
  • Left for PR 12 (listed): thin delegates that keep old import paths working for files outside each area (writeAudit, the host standing module, three chat module paths, the block-pair fragment re-export, the social search wrappers, the forward-audit and upload-lock aliases); symbols still exported only because the transcript cases import them; the registration and organization repository part files that still carry a bare .drizzle.ts name.
  • Deferred (not mechanical, would change tests): routes that call getDb() and build their own services, and the *Overrides test plumbing on the Fastify instance. The one statement still outside a repository is the job-queue depth read in the system health probe, which belongs behind the Jobs adapter.
  • The event-binding fragment stays in the media bindings module, next to the orphan sweep's binding list, so the sweep and the media view authorizer keep reading one definition of "what binds a media asset" (docs/retention-cleanup.md).
  • Kept as fragment modules next to their repositories, as the plan allows: the admin SQL fragments, the registration and cleanup SQL fragments, the category-count and admin-person fragments, the block-pair fragment, and the public pins projection in the report SQL module (moving it would widen the served-key guard's allow-list to the whole report repository).

User-visible copy changes

None.

Tests changed

Only to follow moved paths or renamed symbols; no assertion changed.

  • Import paths: chat attachment tests (2), social mentions, host standing (5 files), media claim uploader, the inbound bounce, chat group, read-ack and tombstone integration tests.
  • erasure-scrubbed-columns.test.ts reads the donation-link statement from the erasure repository, where it now lives.
  • host-export-csv.test.ts reads the roster and check-in queries from the export repository, with the slice markers following the new method names.
  • media-public-served-key.test.ts: the served-key allow-list names the new avatar claim file instead of the old one (same exemption).
  • report-content-routes.test.ts: a comment follows the renamed ownership check.
  • retention-and-phash-pg.test.ts (worker integration): its header comment says where the retention deletes and the duplicate lookup now live.

Verification

  • Every commit typechecks on its own (api and media worker).
  • At this branch: pnpm typecheck, pnpm lint, check:sql clean; api unit suite by explicit path 428 files, 7290 passed, 3 skipped (the transcript suite grew to 939 cases); media-worker unit 300 passed, 2 failed (the known arm64 ffprobe helper issue fixed in Backend test safety net: SQL transcripts, CSRF pairing, authz and characterization tests #92).
  • The SQL transcript proof above.
  • An adversarial review (four passes) compared every moved statement and caller before and after, with the security and privacy lanes (media authorization, content reports, host standing, erasure, data export, retention) checked branch by branch: no behavior, security or performance change; its fixes (a colocation invariant, stale doc rows and comments) are applied.
  • The integration suite was not run (it needs Docker); CI runs it once this PR is retargeted to main. CI runs only on PRs based on main, so it starts here when the PRs below merge.

Staging checks

  • https://api.civfix.dev/readyz answers ready (the readiness ping moved).
  • The regression steps above; the retention sweep and registration retention run on their nightly schedules and log their counts as before.

Not covered

  • The nightly retention sweeps are not triggered by hand; their queries are covered by the transcripts and unit tests.
  • Inbound bounce handling needs a real bounced email; covered by unit tests.

🤖 Generated with Claude Code

@byteful
byteful added this pull request to stack #122 September 24, 2026 17:53
@byteful byteful closed this Sep 24, 2026
@byteful
byteful deleted the chore/backend-campaign-11-sql-repositories branch September 24, 2026 20:13
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