Skip to content

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

Open
hariom888 wants to merge 2 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 2 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 — Checks Failed

Backend — FAIL

Check Result
Lint PASS
Test PASS
Typecheck FAIL

Mobile — SKIP

Check Result
Lint -
Test -

Web — SKIP

Check Result
Check -
Build -

Last updated: Fri, 05 Jun 2026 10:33:45 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.

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