Skip to content

Commit e128aa5

Browse files
committed
feat(cards): complete card management and sharing flow
1 parent d36785a commit e128aa5

3 files changed

Lines changed: 69 additions & 61 deletions

File tree

apps/backend/src/routes/cards.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import * as cardService from '../services/cardService'
22
import { handleDbError } from '../utils/error.util.js';
33
import { createCardSchema ,updateCardSchema, addPlatformLinkSchema} from '../validations/card.validation';
44

5-
import type { CardResponse } from '../services/cardService';
5+
import type { CardResponse, UpdateCardBody } from '../services/cardService';
66
import type { Card } from '@devcard/shared';
77
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
88
import { CardVisibility } from '@prisma/client';
99
import { hashIp } from '../utils/refreshToken';
10-
import { id } from 'zod/v4/locales';
1110

1211
export interface CreateCardBody {
1312
title: string;
@@ -16,10 +15,6 @@ export interface CreateCardBody {
1615
visibility?: CardVisibility
1716
}
1817

19-
interface UpdateCardBody {
20-
title?: string;
21-
linkIds?: string[];
22-
}
2318

2419
interface CardParams {
2520
id: string;
@@ -90,24 +85,28 @@ export async function cardRoutes(app: FastifyInstance): Promise<void> {
9085
});
9186

9287
// ─── Update Card ───
93-
app.put('/:id', async (request: FastifyRequest<{ Params: CardParams; Body: UpdateCardBody }>, reply: FastifyReply): Promise<CardResponse> => {
88+
app.put('/:id/update', async (request: FastifyRequest<{ Params: CardParams; Body: UpdateCardBody }>, reply: FastifyReply) => {
9489
const userId = (request.user as { id: string }).id;
9590
const { id } = request.params;
9691

9792
try {
9893
const parsed = updateCardSchema.safeParse(request.body)
9994
if (!parsed.success) {return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() })}
10095
const updated = await cardService.updateCard(app, userId, id, parsed.data)
101-
if (!updated) {return reply.status(404).send({ error: 'Card not found' })}
102-
return updated
96+
return reply.status(200).send(updated)
10397
} catch (error: any) {
104-
if (error?.code === 'OWNERSHIP') {return reply.status(403).send({ error: 'One or more links do not belong to your account' })}
105-
return handleDbError(error, request, reply)
98+
if(error.code === 'NOT_FOUND'){
99+
return reply.status(404).send({
100+
error: error.message,
101+
});
102+
}
103+
app.log.error(error)
104+
handleDbError(error,request,reply)
106105
}
107106
});
108107

109108
// ─── Delete Card ───
110-
app.delete('/:id', async (request: FastifyRequest<{ Params: CardParams }>, reply: FastifyReply): Promise<void> => {
109+
app.delete('/:id/delete', async (request: FastifyRequest<{ Params: CardParams }>, reply: FastifyReply): Promise<void> => {
111110
const userId = (request.user as { id: string }).id;
112111
const { id } = request.params;
113112

@@ -136,8 +135,14 @@ export async function cardRoutes(app: FastifyInstance): Promise<void> {
136135
try {
137136
const resp = await cardService.setDefaultCard(app, userId, id)
138137
if (!resp) {return reply.status(404).send({ error: 'Card not found' })}
139-
return resp
140-
} catch (error) {
138+
return reply.status(200).send('Default card updated')
139+
} catch (error:any) {
140+
if(error.code === 'NOT_FOUND'){
141+
return reply.status(404).send({
142+
error: error.message,
143+
});
144+
}
145+
app.log.error(error)
141146
return handleDbError(error, request, reply)
142147
}
143148
});

apps/backend/src/services/cardService.ts

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CardVisibility, Prisma } from '@prisma/client';
1+
import { Card, CardVisibility, Prisma } from '@prisma/client';
22

33
import { generateUniqueSlug } from '../utils/slug';
44

@@ -11,6 +11,14 @@ type RawCard = { id: string; title: string; isDefault: boolean; cardLinks: CardL
1111
export type CardResponse = { id: string; title: string; isDefault: boolean; links: unknown[] };
1212

1313

14+
export interface UpdateCardBody{
15+
title?:string;
16+
description?:string;
17+
visibility?: CardVisibility;
18+
qrEnabled?: boolean;
19+
}
20+
21+
1422
function mapCard(card: RawCard): CardResponse {
1523
return {
1624
id: card.id,
@@ -106,50 +114,28 @@ export async function updateCard(
106114
app: FastifyInstance,
107115
userId: string,
108116
id: string,
109-
body: { title?: string; linkIds?: string[] },
110-
): Promise<CardResponse | null> {
111-
const existing = await app.prisma.card.findFirst({ where: { id, userId } });
112-
if (!existing) {
113-
return null;
114-
}
115-
116-
if (body.title) {
117-
await app.prisma.card.update({ where: { id }, data: { title: body.title } });
118-
}
117+
body: UpdateCardBody,
118+
): Promise<Card> {
119+
const {title, description, visibility, qrEnabled} = body
119120

120-
if (body.linkIds) {
121-
if (body.linkIds.length > 0) {
122-
const ownedLinks = await app.prisma.platformLink.findMany({
123-
where: { id: { in: body.linkIds }, userId },
124-
select: { id: true },
125-
});
126-
127-
if (ownedLinks.length !== body.linkIds.length) {
128-
throw Object.assign(new Error('Link ownership mismatch'), { code: 'OWNERSHIP' });
129-
}
121+
const existing = await app.prisma.card.findFirst({ where: { id, userId } });
122+
if (!existing) {
123+
throw Object.assign(new Error('NotFound'), { code: 'NOT_FOUND' });
130124
}
131125

132-
const linkIds = body.linkIds;
133-
await app.prisma.$transaction(async (tx: Prisma.TransactionClient) => {
134-
await tx.cardLink.deleteMany({ where: { cardId: id } });
135-
if (linkIds.length > 0) {
136-
await tx.cardLink.createMany({
137-
data: linkIds.map((linkId, index) => ({ cardId: id, platformLinkId: linkId, displayOrder: index })),
138-
});
126+
const updated = await app.prisma.card.update({
127+
where: {
128+
id,
129+
},
130+
data:{
131+
title,
132+
description,
133+
visibility,
134+
qrEnabled
139135
}
140-
});
141-
}
142-
143-
const updated = (await app.prisma.card.findUnique({
144-
where: { id },
145-
include: { cardLinks: { include: { platformLink: true }, orderBy: { displayOrder: 'asc' } } },
146-
})) as unknown as RawCard | null;
147-
148-
if (!updated) {
149-
return null;
150-
}
136+
})
151137

152-
return mapCard(updated);
138+
return updated;
153139
}
154140

155141
//Delete card service
@@ -184,9 +170,10 @@ export async function deleteCard(app: FastifyInstance, userId: string, id: strin
184170
//Set default card service
185171
export async function setDefaultCard(app: FastifyInstance, userId: string, id: string): Promise<{ message: string } | null> {
186172
const existing = await app.prisma.card.findFirst({ where: { id, userId } });
187-
if (!existing) {
188-
return null;
189-
}
173+
174+
if (!existing) {
175+
throw Object.assign(new Error('NotFound'), { code: 'NOT_FOUND' });
176+
}
190177

191178
await app.prisma.$transaction(async (tx: Prisma.TransactionClient) => {
192179
await tx.card.updateMany({ where: { userId }, data: { isDefault: false } });
@@ -251,6 +238,7 @@ export async function addPlatFormLinks(app: FastifyInstance, userId: string, id:
251238
})
252239
}
253240

241+
//Shares card
254242
export async function shareCard(app: FastifyInstance, userId:string, id: string){
255243
const card = await app.prisma.card.findFirst({
256244
where:{
@@ -279,6 +267,7 @@ export async function shareCard(app: FastifyInstance, userId:string, id: string)
279267
};
280268
}
281269

270+
//Gets share card
282271
export async function getSharedCard(app:FastifyInstance, slug:string){
283272
const card = await app.prisma.card.findUnique({
284273
where: {
@@ -303,6 +292,7 @@ export async function getSharedCard(app:FastifyInstance, slug:string){
303292
return card
304293
}
305294

295+
//Genreate qr
306296
export async function genrateQr(app: FastifyInstance,userId:string, id: string){
307297
const card = await app.prisma.card.findFirst({
308298
where:{

apps/backend/src/validations/card.validation.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,23 @@ export const createCardSchema = z.object({
1919
visibility: z.nativeEnum(CardVisibility).optional(),
2020
});
2121

22-
export const updateCardSchema = z.object({
23-
title: z.string().min(1).max(100).optional(),
24-
linkIds: z.array(z.string().uuid()).optional(),
25-
});
22+
export const updateCardSchema = z
23+
.object({
24+
title: z.string().min(1).max(100).optional(),
25+
description: z.string().min(1).max(100).optional(),
26+
visibility: z.nativeEnum(CardVisibility).optional(),
27+
qrEnabled: z.boolean().optional(),
28+
})
29+
.refine(
30+
(data) =>
31+
data.title !== undefined ||
32+
data.description !== undefined ||
33+
data.visibility !== undefined ||
34+
data.qrEnabled !== undefined,
35+
{
36+
message: 'At least one field must be provided',
37+
}
38+
);
2639

2740
export const addPlatformLinkSchema = z.object({
2841
platformLinkId: z.string().uuid({

0 commit comments

Comments
 (0)