aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/server/repositories/card.ts
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-04 22:31:13 +0900
committernsfisis <nsfisis@gmail.com>2026-02-04 22:43:15 +0900
commita047cdd517efe7693ccd41162f9267f48cd67955 (patch)
tree969c6582d53429085c066aa88881d09f42185aca /src/server/repositories/card.ts
parent87d925c8dfb9c0502a739275df19d1dde8b32230 (diff)
downloadkioku-a047cdd517efe7693ccd41162f9267f48cd67955.tar.gz
kioku-a047cdd517efe7693ccd41162f9267f48cd67955.tar.zst
kioku-a047cdd517efe7693ccd41162f9267f48cd67955.zip
feat(study): enforce newCardsPerDay limit in study API
Split due card fetching into new cards and review cards, applying the deck's newCardsPerDay limit to new cards while leaving review cards unrestricted. New cards are placed before review cards in the response. 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.ts163
1 files changed, 101 insertions, 62 deletions
diff --git a/src/server/repositories/card.ts b/src/server/repositories/card.ts
index 2f14149..6202267 100644
--- a/src/server/repositories/card.ts
+++ b/src/server/repositories/card.ts
@@ -1,4 +1,4 @@
-import { and, eq, isNull, lt, sql } from "drizzle-orm";
+import { and, eq, isNull, lt, lte, ne, sql } from "drizzle-orm";
import { getEndOfStudyDayBoundary } from "../../shared/date.js";
import { db } from "../db/index.js";
import {
@@ -263,69 +263,49 @@ export const cardRepository: CardRepository = {
limit: number,
): Promise<CardForStudy[]> {
const dueCards = await this.findDueCards(deckId, now, limit);
+ return enrichCardsForStudy(dueCards);
+ },
- const cardsForStudy: CardForStudy[] = [];
-
- for (const card of dueCards) {
- // Fetch note to get noteTypeId
- const noteResult = await db
- .select()
- .from(notes)
- .where(and(eq(notes.id, card.noteId), isNull(notes.deletedAt)));
-
- const note = noteResult[0];
- if (!note) {
- // Note was deleted, skip this card
- continue;
- }
-
- // Fetch note type for templates
- const noteTypeResult = await db
- .select({
- frontTemplate: noteTypes.frontTemplate,
- backTemplate: noteTypes.backTemplate,
- })
- .from(noteTypes)
- .where(
- and(eq(noteTypes.id, note.noteTypeId), isNull(noteTypes.deletedAt)),
- );
-
- const noteType = noteTypeResult[0];
- if (!noteType) {
- // Note type was deleted, skip this card
- continue;
- }
-
- // Fetch field values with their field names
- const fieldValuesWithNames = await db
- .select({
- fieldName: noteFieldTypes.name,
- value: noteFieldValues.value,
- })
- .from(noteFieldValues)
- .innerJoin(
- noteFieldTypes,
- eq(noteFieldValues.noteFieldTypeId, noteFieldTypes.id),
- )
- .where(eq(noteFieldValues.noteId, card.noteId));
-
- // Convert to name-value map
- const fieldValuesMap: Record<string, string> = {};
- for (const fv of fieldValuesWithNames) {
- fieldValuesMap[fv.fieldName] = fv.value;
- }
-
- cardsForStudy.push({
- ...card,
- noteType: {
- frontTemplate: noteType.frontTemplate,
- backTemplate: noteType.backTemplate,
- },
- fieldValuesMap,
- });
- }
+ async findDueNewCardsForStudy(
+ deckId: string,
+ now: Date,
+ limit: number,
+ ): Promise<CardForStudy[]> {
+ const result = await db
+ .select()
+ .from(cards)
+ .where(
+ and(
+ eq(cards.deckId, deckId),
+ isNull(cards.deletedAt),
+ lte(cards.due, now),
+ eq(cards.state, CardState.New),
+ ),
+ )
+ .orderBy(cards.due)
+ .limit(limit);
+ return enrichCardsForStudy(result);
+ },
- return cardsForStudy;
+ async findDueReviewCardsForStudy(
+ deckId: string,
+ now: Date,
+ limit: number,
+ ): Promise<CardForStudy[]> {
+ const result = await db
+ .select()
+ .from(cards)
+ .where(
+ and(
+ eq(cards.deckId, deckId),
+ isNull(cards.deletedAt),
+ lte(cards.due, now),
+ ne(cards.state, CardState.New),
+ ),
+ )
+ .orderBy(cards.due)
+ .limit(limit);
+ return enrichCardsForStudy(result);
},
async updateFSRSFields(
@@ -369,3 +349,62 @@ export const cardRepository: CardRepository = {
return result[0];
},
};
+
+async function enrichCardsForStudy(dueCards: Card[]): Promise<CardForStudy[]> {
+ const cardsForStudy: CardForStudy[] = [];
+
+ for (const card of dueCards) {
+ const noteResult = await db
+ .select()
+ .from(notes)
+ .where(and(eq(notes.id, card.noteId), isNull(notes.deletedAt)));
+
+ const note = noteResult[0];
+ if (!note) {
+ continue;
+ }
+
+ const noteTypeResult = await db
+ .select({
+ frontTemplate: noteTypes.frontTemplate,
+ backTemplate: noteTypes.backTemplate,
+ })
+ .from(noteTypes)
+ .where(
+ and(eq(noteTypes.id, note.noteTypeId), isNull(noteTypes.deletedAt)),
+ );
+
+ const noteType = noteTypeResult[0];
+ if (!noteType) {
+ continue;
+ }
+
+ const fieldValuesWithNames = await db
+ .select({
+ fieldName: noteFieldTypes.name,
+ value: noteFieldValues.value,
+ })
+ .from(noteFieldValues)
+ .innerJoin(
+ noteFieldTypes,
+ eq(noteFieldValues.noteFieldTypeId, noteFieldTypes.id),
+ )
+ .where(eq(noteFieldValues.noteId, card.noteId));
+
+ const fieldValuesMap: Record<string, string> = {};
+ for (const fv of fieldValuesWithNames) {
+ fieldValuesMap[fv.fieldName] = fv.value;
+ }
+
+ cardsForStudy.push({
+ ...card,
+ noteType: {
+ frontTemplate: noteType.frontTemplate,
+ backTemplate: noteType.backTemplate,
+ },
+ fieldValuesMap,
+ });
+ }
+
+ return cardsForStudy;
+}