diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-12-07 18:44:05 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-12-07 18:44:05 +0900 |
| commit | b965d9432b4037dd2f65bb4c8690965e090228ca (patch) | |
| tree | d9ad4da71d1f2bdc98e7b1f96b6efaeb58399efc /src/server/repositories | |
| parent | c2609af9d8bac65d3e70b3860160ac8bfe097241 (diff) | |
| download | kioku-b965d9432b4037dd2f65bb4c8690965e090228ca.tar.gz kioku-b965d9432b4037dd2f65bb4c8690965e090228ca.tar.zst kioku-b965d9432b4037dd2f65bb4c8690965e090228ca.zip | |
feat(server): add study session API with FSRS integration
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 <noreply@anthropic.com>
Diffstat (limited to 'src/server/repositories')
| -rw-r--r-- | src/server/repositories/card.ts | 63 | ||||
| -rw-r--r-- | src/server/repositories/index.ts | 1 | ||||
| -rw-r--r-- | src/server/repositories/review-log.ts | 32 | ||||
| -rw-r--r-- | src/server/repositories/types.ts | 41 |
4 files changed, 136 insertions, 1 deletions
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<Card[]> { + 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<Card | undefined> { + 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]; + }, }; diff --git a/src/server/repositories/index.ts b/src/server/repositories/index.ts index 298666e..e909a9b 100644 --- a/src/server/repositories/index.ts +++ b/src/server/repositories/index.ts @@ -1,5 +1,6 @@ export { cardRepository } from "./card.js"; export { deckRepository } from "./deck.js"; export { refreshTokenRepository } from "./refresh-token.js"; +export { reviewLogRepository } from "./review-log.js"; export * from "./types.js"; export { userRepository } from "./user.js"; diff --git a/src/server/repositories/review-log.ts b/src/server/repositories/review-log.ts new file mode 100644 index 0000000..c8950d6 --- /dev/null +++ b/src/server/repositories/review-log.ts @@ -0,0 +1,32 @@ +import { db } from "../db/index.js"; +import { reviewLogs } from "../db/schema.js"; +import type { ReviewLog, ReviewLogRepository } from "./types.js"; + +export const reviewLogRepository: ReviewLogRepository = { + async create(data: { + cardId: string; + userId: string; + rating: number; + state: number; + scheduledDays: number; + elapsedDays: number; + durationMs?: number | null; + }): Promise<ReviewLog> { + const [reviewLog] = await db + .insert(reviewLogs) + .values({ + cardId: data.cardId, + userId: data.userId, + rating: data.rating, + state: data.state, + scheduledDays: data.scheduledDays, + elapsedDays: data.elapsedDays, + durationMs: data.durationMs ?? null, + }) + .returning(); + if (!reviewLog) { + throw new Error("Failed to create review log"); + } + return reviewLog; + }, +}; diff --git a/src/server/repositories/types.ts b/src/server/repositories/types.ts index 1e8ba21..c5ca946 100644 --- a/src/server/repositories/types.ts +++ b/src/server/repositories/types.ts @@ -120,4 +120,45 @@ export interface CardRepository { }, ): Promise<Card | undefined>; softDelete(id: string, deckId: string): Promise<boolean>; + findDueCards(deckId: string, now: Date, limit: number): Promise<Card[]>; + 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<Card | undefined>; +} + +export interface ReviewLog { + id: string; + cardId: string; + userId: string; + rating: number; + state: number; + scheduledDays: number; + elapsedDays: number; + reviewedAt: Date; + durationMs: number | null; + syncVersion: number; +} + +export interface ReviewLogRepository { + create(data: { + cardId: string; + userId: string; + rating: number; + state: number; + scheduledDays: number; + elapsedDays: number; + durationMs?: number | null; + }): Promise<ReviewLog>; } |
