aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/server/repositories/card.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/repositories/card.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/repositories/card.ts')
-rw-r--r--src/server/repositories/card.ts102
1 files changed, 102 insertions, 0 deletions
diff --git a/src/server/repositories/card.ts b/src/server/repositories/card.ts
new file mode 100644
index 0000000..0a47c50
--- /dev/null
+++ b/src/server/repositories/card.ts
@@ -0,0 +1,102 @@
+import { and, eq, isNull, sql } from "drizzle-orm";
+import { db } from "../db/index.js";
+import { CardState, cards } from "../db/schema.js";
+import type { Card, CardRepository } from "./types.js";
+
+export const cardRepository: CardRepository = {
+ async findByDeckId(deckId: string): Promise<Card[]> {
+ const result = await db
+ .select()
+ .from(cards)
+ .where(and(eq(cards.deckId, deckId), isNull(cards.deletedAt)));
+ return result;
+ },
+
+ async findById(id: string, deckId: string): Promise<Card | undefined> {
+ const result = await db
+ .select()
+ .from(cards)
+ .where(
+ and(
+ eq(cards.id, id),
+ eq(cards.deckId, deckId),
+ isNull(cards.deletedAt),
+ ),
+ );
+ return result[0];
+ },
+
+ async create(
+ deckId: string,
+ data: {
+ front: string;
+ back: string;
+ },
+ ): Promise<Card> {
+ const [card] = await db
+ .insert(cards)
+ .values({
+ deckId,
+ front: data.front,
+ back: data.back,
+ state: CardState.New,
+ due: new Date(),
+ stability: 0,
+ difficulty: 0,
+ elapsedDays: 0,
+ scheduledDays: 0,
+ reps: 0,
+ lapses: 0,
+ })
+ .returning();
+ if (!card) {
+ throw new Error("Failed to create card");
+ }
+ return card;
+ },
+
+ async update(
+ id: string,
+ deckId: string,
+ data: {
+ front?: string;
+ back?: string;
+ },
+ ): Promise<Card | undefined> {
+ const result = await db
+ .update(cards)
+ .set({
+ ...data,
+ updatedAt: new Date(),
+ syncVersion: sql`${cards.syncVersion} + 1`,
+ })
+ .where(
+ and(
+ eq(cards.id, id),
+ eq(cards.deckId, deckId),
+ isNull(cards.deletedAt),
+ ),
+ )
+ .returning();
+ return result[0];
+ },
+
+ async softDelete(id: string, deckId: string): Promise<boolean> {
+ const result = await db
+ .update(cards)
+ .set({
+ deletedAt: new Date(),
+ updatedAt: new Date(),
+ syncVersion: sql`${cards.syncVersion} + 1`,
+ })
+ .where(
+ and(
+ eq(cards.id, id),
+ eq(cards.deckId, deckId),
+ isNull(cards.deletedAt),
+ ),
+ )
+ .returning({ id: cards.id });
+ return result.length > 0;
+ },
+};