aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/server/repositories
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-12-07 18:44:05 +0900
committernsfisis <nsfisis@gmail.com>2025-12-07 18:44:05 +0900
commitb965d9432b4037dd2f65bb4c8690965e090228ca (patch)
treed9ad4da71d1f2bdc98e7b1f96b6efaeb58399efc /src/server/repositories
parentc2609af9d8bac65d3e70b3860160ac8bfe097241 (diff)
downloadkioku-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.ts63
-rw-r--r--src/server/repositories/index.ts1
-rw-r--r--src/server/repositories/review-log.ts32
-rw-r--r--src/server/repositories/types.ts41
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>;
}