diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-12-07 18:14:02 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-12-07 18:14:02 +0900 |
| commit | 0b0e7e802fcb50652c3e9912363d996a039d56d8 (patch) | |
| tree | a3fc98441f638159ddedf98c637ed6045c2de382 /src/server/routes/cards.ts | |
| parent | 2ca8bfadd49fb8e5f45b6324cff13c35a2858bb7 (diff) | |
| download | kioku-0b0e7e802fcb50652c3e9912363d996a039d56d8.tar.gz kioku-0b0e7e802fcb50652c3e9912363d996a039d56d8.tar.zst kioku-0b0e7e802fcb50652c3e9912363d996a039d56d8.zip | |
feat(server): add card CRUD endpoints
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 <noreply@anthropic.com>
Diffstat (limited to 'src/server/routes/cards.ts')
| -rw-r--r-- | src/server/routes/cards.ts | 130 |
1 files changed, 130 insertions, 0 deletions
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, +}); |
