From b965d9432b4037dd2f65bb4c8690965e090228ca Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 7 Dec 2025 18:44:05 +0900 Subject: feat(server): add study session API with FSRS integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement study endpoints for spaced repetition learning: - GET /api/decks/:deckId/study to fetch due cards - POST /api/decks/:deckId/study/:cardId to submit reviews - Integrate ts-fsrs library for scheduling algorithm - Add ReviewLog repository for tracking review history 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/server/repositories/card.ts | 63 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) (limited to 'src/server/repositories/card.ts') diff --git a/src/server/repositories/card.ts b/src/server/repositories/card.ts index 0a47c50..76c9d30 100644 --- a/src/server/repositories/card.ts +++ b/src/server/repositories/card.ts @@ -1,4 +1,4 @@ -import { and, eq, isNull, sql } from "drizzle-orm"; +import { and, eq, isNull, lte, sql } from "drizzle-orm"; import { db } from "../db/index.js"; import { CardState, cards } from "../db/schema.js"; import type { Card, CardRepository } from "./types.js"; @@ -99,4 +99,65 @@ export const cardRepository: CardRepository = { .returning({ id: cards.id }); return result.length > 0; }, + + async findDueCards( + deckId: string, + now: Date, + limit: number, + ): Promise { + const result = await db + .select() + .from(cards) + .where( + and( + eq(cards.deckId, deckId), + isNull(cards.deletedAt), + lte(cards.due, now), + ), + ) + .orderBy(cards.due) + .limit(limit); + return result; + }, + + async updateFSRSFields( + id: string, + deckId: string, + data: { + state: number; + due: Date; + stability: number; + difficulty: number; + elapsedDays: number; + scheduledDays: number; + reps: number; + lapses: number; + lastReview: Date; + }, + ): Promise { + const result = await db + .update(cards) + .set({ + state: data.state, + due: data.due, + stability: data.stability, + difficulty: data.difficulty, + elapsedDays: data.elapsedDays, + scheduledDays: data.scheduledDays, + reps: data.reps, + lapses: data.lapses, + lastReview: data.lastReview, + 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]; + }, }; -- cgit v1.2.3-70-g09d2