From 0b0e7e802fcb50652c3e9912363d996a039d56d8 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 7 Dec 2025 18:14:02 +0900 Subject: feat(server): add card CRUD endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement card management API with create, read, update, and delete operations. Cards are nested under decks (/api/decks/:deckId/cards) with deck ownership verification on all operations. - Add Card interface and CardRepository to repository types - Create cardRepository with CRUD operations and soft delete - Add card routes with Zod validation and auth middleware - Include 29 tests covering all endpoints and error cases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/server/routes/cards.ts | 130 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/server/routes/cards.ts (limited to 'src/server/routes/cards.ts') diff --git a/src/server/routes/cards.ts b/src/server/routes/cards.ts new file mode 100644 index 0000000..6fb259b --- /dev/null +++ b/src/server/routes/cards.ts @@ -0,0 +1,130 @@ +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { z } from "zod"; +import { authMiddleware, Errors, getAuthUser } from "../middleware/index.js"; +import { + type CardRepository, + cardRepository, + type DeckRepository, + deckRepository, +} from "../repositories/index.js"; +import { createCardSchema, updateCardSchema } from "../schemas/index.js"; + +export interface CardDependencies { + cardRepo: CardRepository; + deckRepo: DeckRepository; +} + +const deckIdParamSchema = z.object({ + deckId: z.string().uuid(), +}); + +const cardIdParamSchema = z.object({ + deckId: z.string().uuid(), + cardId: z.string().uuid(), +}); + +export function createCardsRouter(deps: CardDependencies) { + const { cardRepo, deckRepo } = deps; + + return new Hono() + .use("*", authMiddleware) + .get("/", zValidator("param", deckIdParamSchema), async (c) => { + const user = getAuthUser(c); + const { deckId } = c.req.valid("param"); + + // Verify deck ownership + const deck = await deckRepo.findById(deckId, user.id); + if (!deck) { + throw Errors.notFound("Deck not found", "DECK_NOT_FOUND"); + } + + const cards = await cardRepo.findByDeckId(deckId); + return c.json({ cards }, 200); + }) + .post( + "/", + zValidator("param", deckIdParamSchema), + zValidator("json", createCardSchema), + async (c) => { + const user = getAuthUser(c); + const { deckId } = c.req.valid("param"); + const data = c.req.valid("json"); + + // Verify deck ownership + const deck = await deckRepo.findById(deckId, user.id); + if (!deck) { + throw Errors.notFound("Deck not found", "DECK_NOT_FOUND"); + } + + const card = await cardRepo.create(deckId, { + front: data.front, + back: data.back, + }); + + return c.json({ card }, 201); + }, + ) + .get("/:cardId", zValidator("param", cardIdParamSchema), async (c) => { + const user = getAuthUser(c); + const { deckId, cardId } = c.req.valid("param"); + + // Verify deck ownership + const deck = await deckRepo.findById(deckId, user.id); + if (!deck) { + throw Errors.notFound("Deck not found", "DECK_NOT_FOUND"); + } + + const card = await cardRepo.findById(cardId, deckId); + if (!card) { + throw Errors.notFound("Card not found", "CARD_NOT_FOUND"); + } + + return c.json({ card }, 200); + }) + .put( + "/:cardId", + zValidator("param", cardIdParamSchema), + zValidator("json", updateCardSchema), + async (c) => { + const user = getAuthUser(c); + const { deckId, cardId } = c.req.valid("param"); + const data = c.req.valid("json"); + + // Verify deck ownership + const deck = await deckRepo.findById(deckId, user.id); + if (!deck) { + throw Errors.notFound("Deck not found", "DECK_NOT_FOUND"); + } + + const card = await cardRepo.update(cardId, deckId, data); + if (!card) { + throw Errors.notFound("Card not found", "CARD_NOT_FOUND"); + } + + return c.json({ card }, 200); + }, + ) + .delete("/:cardId", zValidator("param", cardIdParamSchema), async (c) => { + const user = getAuthUser(c); + const { deckId, cardId } = c.req.valid("param"); + + // Verify deck ownership + const deck = await deckRepo.findById(deckId, user.id); + if (!deck) { + throw Errors.notFound("Deck not found", "DECK_NOT_FOUND"); + } + + const deleted = await cardRepo.softDelete(cardId, deckId); + if (!deleted) { + throw Errors.notFound("Card not found", "CARD_NOT_FOUND"); + } + + return c.json({ success: true }, 200); + }); +} + +export const cards = createCardsRouter({ + cardRepo: cardRepository, + deckRepo: deckRepository, +}); -- cgit v1.2.3-70-g09d2