aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/server/routes/cards.ts
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-12-07 18:14:02 +0900
committernsfisis <nsfisis@gmail.com>2025-12-07 18:14:02 +0900
commit0b0e7e802fcb50652c3e9912363d996a039d56d8 (patch)
treea3fc98441f638159ddedf98c637ed6045c2de382 /src/server/routes/cards.ts
parent2ca8bfadd49fb8e5f45b6324cff13c35a2858bb7 (diff)
downloadkioku-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.ts130
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,
+});