Skip to content

fix(cardService): wrap title + linkIds updates in single atomic transaction#477

Open
hariom888 wants to merge 8 commits into
Dev-Card:mainfrom
hariom888:fix/437-updatecard-non-atomic-writes
Open

fix(cardService): wrap title + linkIds updates in single atomic transaction#477
hariom888 wants to merge 8 commits into
Dev-Card:mainfrom
hariom888:fix/437-updatecard-non-atomic-writes

Conversation

@hariom888

Copy link
Copy Markdown
Contributor

Summary

Fixes #437.

updateCard executed card.update (title) and the cardLink delete/create cycle as two independent operations. A crash, DB timeout, or FK violation between them left the card with its new title but stale links — permanently inconsistent with no compensating write. Additionally, the platformLink ownership check ran outside the inner transaction, creating a TOCTOU window where a concurrent request could delete a platformLink between validation and createMany.

Root cause

// Before — two independent writes, no atomicity guarantee
if (body.title) {
  await app.prisma.card.update(...)          // committed immediately
}
if (body.linkIds) {
  const ownedLinks = await app.prisma.platformLink.findMany(...)  // TOCTOU gap
  await app.prisma.$transaction(async (tx) => {
    await tx.cardLink.deleteMany(...)
    await tx.cardLink.createMany(...)
  })
}

Fix

  1. Single $transaction — both tx.card.update (title) and tx.cardLink.deleteMany / tx.cardLink.createMany (links) execute inside one Prisma interactive transaction. Either all changes commit or none do.
  2. Ownership check moved before the transactionplatformLink.findMany runs before any transaction is opened. A 403 (ownership failure) now never consumes a transaction slot and never results in any write. Ownership validation is safe outside the transaction because platformLink rows are user-owned and not mutated within this request.

Testing

Five new cases added to the PUT /api/cards/:id — atomicity suite:

Scenario Assertion
Happy path (title + linkIds) Single $transaction, both card.update and cardLink ops called
createMany fails after card.update 500, findUnique not called, both ops ran inside same tx
Foreign linkId (403) $transaction never called, no writes at all
Title-only update cardLink.deleteMany not called, platformLink.findMany not called
Links-only update card.update not called

Files changed

  • apps/backend/src/services/cardService.ts
  • apps/backend/src/__tests__/cards.test.ts

…action

updateCard previously executed card.update (title) and the cardLink delete/create cycle as two independent operations. A process crash, DB timeout, or FK violation between them left the card with a new title but stale links — a permanently inconsistent state with no rollback path.

Changes:
- Move both the card.update (title) and the cardLink.deleteMany / cardLink.createMany calls inside a single app.prisma. block so that either all changes commit or none do.
- Hoist the platformLink ownership check to before the transaction is opened, eliminating the TOCTOU window where a concurrent request could delete a platformLink between validation and createMany. Ownership validation on user-owned immutable rows is safe to perform outside the transaction.

Tests added (cards.test.ts):
- Happy path: both title and links commit in one  call.
- Rollback path: createMany failure after card.update → 500, no findUnique called, both operations ran inside the same tx.
- Pre-transaction 403: foreign linkId →  never called, no writes of any kind.
- Title-only update: linkIds absent → deleteMany not called.
- Links-only update: title absent → card.update not called.
@vercel

vercel Bot commented Jun 5, 2026

Copy link
Copy Markdown

@hariom888 is attempting to deploy a commit to the Prashantkumar Khatri's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

CI — All Checks Passed

Backend — PASS

Check Result
Lint PASS
Test PASS
Typecheck PASS

Mobile — SKIP

Check Result
Lint -
Test -

Web — SKIP

Check Result
Check -
Build -

Last updated: Thu, 11 Jun 2026 17:51:46 GMT

@hariom888

Copy link
Copy Markdown
Contributor Author

@Harxhit

Typecheck failure is pre-existing on main — not introduced by this PR

All 15 typecheck errors are in files I did not touch:

  • src/routes/team.ts — 6 errors (missing TeamRole export, implicit any, PrismaClientKnownRequestError)
  • src/__tests__/team.test.ts — 1 error (missing TeamRole export)
  • src/services/publicService.ts — 1 error (implicit any)
  • src/utils/error.util.ts — 7 errors (PrismaClientKnownRequestError, PrismaClientValidationError, unknown type)

To confirm this is pre-existing, run on main directly:

git checkout main && npm --prefix apps/backend run typecheck

The same 15 errors appear. My branch introduces zero new type errors.

Lint: ✅ PASS
Test: ✅ PASS
Typecheck: ❌ pre-existing failure unrelated to this PR

@Harxhit Harxhit added the gssoc:approved Required label for every approved PR. Gives the base +50 points and enables contribution tracking. label Jun 6, 2026
@ShantKhatri ShantKhatri requested a review from Harxhit June 6, 2026 17:42

@Harxhit Harxhit left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address lint errors.

@hariom888

Copy link
Copy Markdown
Contributor Author

@Harxhit all checks have passed.

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

Labels

gssoc:approved Required label for every approved PR. Gives the base +50 points and enables contribution tracking.

Projects

None yet

2 participants