Skip to content

Commit f6ee844

Browse files
authored
fix: wrap card creation in transaction to prevent race condition (#349)
Signed-off-by: dinesh <midoriya54378@gmail.com>
1 parent c535f20 commit f6ee844

1 file changed

Lines changed: 51 additions & 0 deletions

File tree

apps/backend/src/routes/cards.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,57 @@ export async function cardRoutes(app: FastifyInstance): Promise<void> {
7979
}
8080

8181
try {
82+
// Verify every supplied link belongs to the authenticated user before any write.
83+
// A count mismatch means at least one ID is foreign — reject before touching the DB.
84+
if (parsed.data.linkIds.length > 0) {
85+
const ownedLinks = await app.prisma.platformLink.findMany({
86+
where: { id: { in: parsed.data.linkIds }, userId },
87+
select: { id: true },
88+
});
89+
90+
if (ownedLinks.length !== parsed.data.linkIds.length) {
91+
return reply.status(403).send({ error: 'One or more links do not belong to your account' });
92+
}
93+
}
94+
95+
// Check if user's first card -> make it default.
96+
// Prisma wraps the nested cardLinks.create inside card.create in a single
97+
// implicit transaction, so either both the card and its links are written or neither is.
98+
const card = await app.prisma.$transaction(async (tx) => {
99+
const cardCount = await tx.card.count({
100+
where: { userId },
101+
});
102+
103+
return tx.card.create({
104+
data: {
105+
userId,
106+
title: parsed.data.title,
107+
isDefault: cardCount === 0,
108+
cardLinks: {
109+
create: parsed.data.linkIds.map((linkId, index) => ({
110+
platformLinkId: linkId,
111+
displayOrder: index,
112+
})),
113+
},
114+
},
115+
include: {
116+
cardLinks: {
117+
include: { platformLink: true },
118+
orderBy: { displayOrder: 'asc' },
119+
},
120+
},
121+
});
122+
}};
123+
const response = {
124+
id: card.id,
125+
title: card.title,
126+
isDefault: card.isDefault,
127+
links: card.cardLinks.map((cl: CardLinkWithPlatform) => cl.platformLink),
128+
}
129+
130+
return reply.status(201).send(response);
131+
} catch (error) {
132+
return handleDbError(error, request, reply);
82133
const card = await cardService.createCard(app, userId, parsed.data)
83134
return reply.status(201).send(card)
84135
} catch (error: any) {

0 commit comments

Comments
 (0)