Multi-step writes run without transactions (partial campaign/character on failure) #35

Closed
opened 2026-08-23 05:42:05 +00:00 by nasandre · 2 comments
Owner

Medium (data integrity)

A transaction helper exists but is unused:

  • `backend/src/config/database.ts:217` — `withTransaction` (BEGIN/COMMIT/ROLLBACK)
  • Zero call sites in services

Affected flows:

  • `campaignService.createCampaign` (:440-487): INSERT campaign → replacePlayers → creator insert → replaceEvents/systems/routes/factions/contacts/sessions — 9 sequential writes; a failure mid-way leaves a partial campaign
  • `campaignService.updateCampaign` (:489-546): UPDATE + up to 7 replace* calls
  • `characterService.createCharacter`: INSERT character + skills + augments + equipment loop
  • `replacePlayers` deletes all rows then re-inserts in a loop — failure after DELETE loses all players

Suggested fix

Wrap create/update flows in `withTransaction` (needs the query helper to accept an optional client).

## Medium (data integrity) A transaction helper exists but is unused: - \`backend/src/config/database.ts:217\` — \`withTransaction\` (BEGIN/COMMIT/ROLLBACK) - Zero call sites in services Affected flows: - \`campaignService.createCampaign\` (:440-487): INSERT campaign → replacePlayers → creator insert → replaceEvents/systems/routes/factions/contacts/sessions — 9 sequential writes; a failure mid-way leaves a partial campaign - \`campaignService.updateCampaign\` (:489-546): UPDATE + up to 7 replace* calls - \`characterService.createCharacter\`: INSERT character + skills + augments + equipment loop - \`replacePlayers\` deletes all rows then re-inserts in a loop — failure after DELETE loses all players ## Suggested fix Wrap create/update flows in \`withTransaction\` (needs the query helper to accept an optional client).
Author
Owner

✅ Fixed:

  • Added withTransaction(fn) using AsyncLocalStorage — any query() issued inside joins the same Postgres transaction automatically (zero call-site churn)
  • Wrapped: createCampaign (INSERT + players + events + systems + routes + factions + contacts + sessions), updateCampaign child replacement block, createCharacter (row + skills + talents + augments + kit)
  • SQLite path gets BEGIN/COMMIT/ROLLBACK as well
✅ **Fixed**: - Added `withTransaction(fn)` using AsyncLocalStorage — any `query()` issued inside joins the same Postgres transaction automatically (zero call-site churn) - Wrapped: `createCampaign` (INSERT + players + events + systems + routes + factions + contacts + sessions), `updateCampaign` child replacement block, `createCharacter` (row + skills + talents + augments + kit) - SQLite path gets BEGIN/COMMIT/ROLLBACK as well
Author
Owner

✅ Fixed (commit 05b5453) — new withTransaction() helper using AsyncLocalStorage; any query() inside automatically joins the transaction (zero call-site changes). Wrapped createCampaign (all 9 writes), updateCampaign child replacement, createCharacter (row + skills + talents + augments + kit). SQLite gets BEGIN/COMMIT/ROLLBACK too.

✅ **Fixed** (commit `05b5453`) — new `withTransaction()` helper using AsyncLocalStorage; any `query()` inside automatically joins the transaction (zero call-site changes). Wrapped createCampaign (all 9 writes), updateCampaign child replacement, createCharacter (row + skills + talents + augments + kit). SQLite gets BEGIN/COMMIT/ROLLBACK too.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
nasandre/wh40-rogue-trader#35
No description provided.