aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/server/repositories
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
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')
-rw-r--r--src/server/repositories/card.ts102
-rw-r--r--src/server/repositories/index.ts1
-rw-r--r--src/server/repositories/types.ts44
3 files changed, 147 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;
+ },
+};
diff --git a/src/server/repositories/index.ts b/src/server/repositories/index.ts
index 9a703ab..298666e 100644
--- a/src/server/repositories/index.ts
+++ b/src/server/repositories/index.ts
@@ -1,3 +1,4 @@
+export { cardRepository } from "./card.js";
export { deckRepository } from "./deck.js";
export { refreshTokenRepository } from "./refresh-token.js";
export * from "./types.js";
diff --git a/src/server/repositories/types.ts b/src/server/repositories/types.ts
index 740fa29..1e8ba21 100644
--- a/src/server/repositories/types.ts
+++ b/src/server/repositories/types.ts
@@ -77,3 +77,47 @@ export interface DeckRepository {
): Promise<Deck | undefined>;
softDelete(id: string, userId: string): Promise<boolean>;
}
+
+export interface Card {
+ id: string;
+ deckId: string;
+ front: string;
+ back: string;
+
+ // FSRS fields
+ state: number;
+ due: Date;
+ stability: number;
+ difficulty: number;
+ elapsedDays: number;
+ scheduledDays: number;
+ reps: number;
+ lapses: number;
+ lastReview: Date | null;
+
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: Date | null;
+ syncVersion: number;
+}
+
+export interface CardRepository {
+ findByDeckId(deckId: string): Promise<Card[]>;
+ findById(id: string, deckId: string): Promise<Card | undefined>;
+ create(
+ deckId: string,
+ data: {
+ front: string;
+ back: string;
+ },
+ ): Promise<Card>;
+ update(
+ id: string,
+ deckId: string,
+ data: {
+ front?: string;
+ back?: string;
+ },
+ ): Promise<Card | undefined>;
+ softDelete(id: string, deckId: string): Promise<boolean>;
+}