Skip to content

Commit 2b29bd0

Browse files
committed
feat(cards): update card creation flow and validation
1 parent ba19ba2 commit 2b29bd0

4 files changed

Lines changed: 58 additions & 28 deletions

File tree

apps/backend/src/routes/cards.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as cardService from '../services/cardService'
22
import { handleDbError } from '../utils/error.util.js';
3-
import { createCardSchema, updateCardSchema } from '../utils/validators.js';
3+
import { createCardSchema ,updateCardSchema} from '../validations/card.validation';
44

55
import type { CardResponse } from '../services/cardService';
66
import type { Card } from '@devcard/shared';
@@ -67,8 +67,7 @@ export async function cardRoutes(app: FastifyInstance): Promise<void> {
6767
}
6868
});
6969

70-
// ─── Create Card ───
71-
70+
// ─── Creates Card ───
7271
app.post('/', async (request: FastifyRequest<{ Body: CreateCardBody }>, reply: FastifyReply): Promise<Card | void> => {
7372
const userId = (request.user as { id: string }).id;
7473
const parsed = createCardSchema.safeParse(request.body);
@@ -81,7 +80,7 @@ export async function cardRoutes(app: FastifyInstance): Promise<void> {
8180
const card = await cardService.createCard(app, userId, parsed.data)
8281
return reply.status(201).send(card)
8382
} catch (error: any) {
84-
if (error?.code === 'OWNERSHIP') {return reply.status(403).send({ error: 'One or more links do not belong to your account' })}
83+
if (error?.code === 'OWNERSHIP'){return reply.status(403).send({ error: 'One or more links do not belong to your account' })}
8584
return handleDbError(error, request, reply)
8685
}
8786
});

apps/backend/src/services/cardService.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import type { Prisma } from '@prisma/client';
1+
import { CardVisibility, type Prisma } from '@prisma/client';
2+
3+
import { generateUniqueSlug } from '../utils/slug';
4+
25
import type { FastifyInstance } from 'fastify';
36

47
type CardLinkResponse = { platformLink: unknown };
@@ -25,17 +28,27 @@ export async function listCards(app: FastifyInstance, userId: string): Promise<C
2528
return cards.map(mapCard);
2629
}
2730

28-
export async function createCard(app: FastifyInstance, userId: string, body: { title: string; linkIds: string[] }): Promise<CardResponse> {
29-
if (body.linkIds.length > 0) {
30-
const ownedLinks = await app.prisma.platformLink.findMany({
31-
where: { id: { in: body.linkIds }, userId },
32-
select: { id: true },
33-
});
31+
export async function createCard(app: FastifyInstance, userId: string, body: { title: string; linkIds: string[] , description?: string, visibility?: CardVisibility}): Promise<CardResponse> {
32+
const {title , description , linkIds , visibility} = body
33+
34+
const ownedLinks = await app.prisma.platformLink.findMany({
35+
where: { id: { in: linkIds }, userId },
36+
select: { id: true },
37+
});
38+
39+
if (ownedLinks.length !== linkIds.length) {
40+
throw Object.assign(new Error('Link ownership mismatch'), { code: 'OWNERSHIP' });
41+
}
42+
43+
const finalSlug = await generateUniqueSlug(title, async(slug) => {
44+
const existing = await app.prisma.card.findUnique({
45+
where: {
46+
slug
47+
}
48+
})
49+
return !!existing
50+
})
3451

35-
if (ownedLinks.length !== body.linkIds.length) {
36-
throw Object.assign(new Error('Link ownership mismatch'), { code: 'OWNERSHIP' });
37-
}
38-
}
3952

4053
const maxRetries = 3;
4154
for (let attempt = 1; attempt <= maxRetries; attempt++) {
@@ -47,10 +60,13 @@ export async function createCard(app: FastifyInstance, userId: string, body: { t
4760
return tx.card.create({
4861
data: {
4962
userId,
50-
title: body.title,
63+
title,
64+
slug: finalSlug,
5165
isDefault: cardCount === 0,
66+
description,
67+
visibility: visibility ?? CardVisibility.PUBLIC,
5268
cardLinks: {
53-
create: body.linkIds.map((linkId, index) => ({ platformLinkId: linkId, displayOrder: index })),
69+
create: linkIds.map((linkId, index) => ({ platformLinkId: linkId, displayOrder: index })),
5470
},
5571
},
5672
include: { cardLinks: { include: { platformLink: true }, orderBy: { displayOrder: 'asc' } } },
@@ -72,9 +88,8 @@ export async function createCard(app: FastifyInstance, userId: string, body: { t
7288
) {
7389
continue;
7490
}
75-
7691
app.log.error(error);
77-
throw error;
92+
throw error
7893
}
7994
}
8095

apps/backend/src/utils/validators.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,3 @@ export const reorderLinksSchema = z.object({
4545
),
4646
});
4747

48-
export const createCardSchema = z.object({
49-
title: z.string().min(1).max(100),
50-
linkIds: z.array(z.string().uuid()),
51-
});
52-
53-
export const updateCardSchema = z.object({
54-
title: z.string().min(1).max(100).optional(),
55-
linkIds: z.array(z.string().uuid()).optional(),
56-
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { CardVisibility } from '@prisma/client';
2+
import {z} from 'zod'
3+
4+
export const createCardSchema = z.object({
5+
title: z.string().min(1).max(100),
6+
7+
linkIds: z
8+
.array(z.string().uuid())
9+
.nonempty({
10+
message: 'At least one link is required',
11+
})
12+
.refine(
13+
(ids) => new Set(ids).size === ids.length,
14+
{
15+
message: 'Duplicate links are not allowed',
16+
}
17+
),
18+
description: z.string().min(1).max(100).optional(),
19+
visibility: z.nativeEnum(CardVisibility).optional(),
20+
});
21+
22+
export const updateCardSchema = z.object({
23+
title: z.string().min(1).max(100).optional(),
24+
linkIds: z.array(z.string().uuid()).optional(),
25+
});

0 commit comments

Comments
 (0)