From 797ef2fcfaa7ac63355c13809a644401a76250bc Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 7 Dec 2025 17:37:08 +0900 Subject: feat(server): add Deck CRUD endpoints with tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement complete Deck management API: - GET /api/decks - List user's decks - POST /api/decks - Create new deck - GET /api/decks/:id - Get deck by ID - PUT /api/decks/:id - Update deck - DELETE /api/decks/:id - Soft delete deck All endpoints require authentication and scope data to the authenticated user. Includes 22 unit tests covering success and error cases. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/server/routes/decks.ts | 82 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/server/routes/decks.ts (limited to 'src/server/routes/decks.ts') diff --git a/src/server/routes/decks.ts b/src/server/routes/decks.ts new file mode 100644 index 0000000..4604ea9 --- /dev/null +++ b/src/server/routes/decks.ts @@ -0,0 +1,82 @@ +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { z } from "zod"; +import { authMiddleware, Errors, getAuthUser } from "../middleware/index.js"; +import { type DeckRepository, deckRepository } from "../repositories/index.js"; +import { createDeckSchema, updateDeckSchema } from "../schemas/index.js"; + +export interface DeckDependencies { + deckRepo: DeckRepository; +} + +const deckIdParamSchema = z.object({ + id: z.string().uuid(), +}); + +export function createDecksRouter(deps: DeckDependencies) { + const { deckRepo } = deps; + + return new Hono() + .use("*", authMiddleware) + .get("/", async (c) => { + const user = getAuthUser(c); + const decks = await deckRepo.findByUserId(user.id); + return c.json({ decks }, 200); + }) + .post("/", zValidator("json", createDeckSchema), async (c) => { + const user = getAuthUser(c); + const data = c.req.valid("json"); + + const deck = await deckRepo.create({ + userId: user.id, + name: data.name, + description: data.description, + newCardsPerDay: data.newCardsPerDay, + }); + + return c.json({ deck }, 201); + }) + .get("/:id", zValidator("param", deckIdParamSchema), async (c) => { + const user = getAuthUser(c); + const { id } = c.req.valid("param"); + + const deck = await deckRepo.findById(id, user.id); + if (!deck) { + throw Errors.notFound("Deck not found", "DECK_NOT_FOUND"); + } + + return c.json({ deck }, 200); + }) + .put( + "/:id", + zValidator("param", deckIdParamSchema), + zValidator("json", updateDeckSchema), + async (c) => { + const user = getAuthUser(c); + const { id } = c.req.valid("param"); + const data = c.req.valid("json"); + + const deck = await deckRepo.update(id, user.id, data); + if (!deck) { + throw Errors.notFound("Deck not found", "DECK_NOT_FOUND"); + } + + return c.json({ deck }, 200); + }, + ) + .delete("/:id", zValidator("param", deckIdParamSchema), async (c) => { + const user = getAuthUser(c); + const { id } = c.req.valid("param"); + + const deleted = await deckRepo.softDelete(id, user.id); + if (!deleted) { + throw Errors.notFound("Deck not found", "DECK_NOT_FOUND"); + } + + return c.json({ success: true }, 200); + }); +} + +export const decks = createDecksRouter({ + deckRepo: deckRepository, +}); -- cgit v1.2.3-70-g09d2