refactor(api): review-row-Erstellung extrahieren + QW-Fixes

- makeInitialReviewRows() in lib/reviews.ts: eliminiert 45 Zeilen
  Duplikat aus cards.ts, decks-generate.ts und tools.ts
- /distractors: Query-Param cardId → card_id (snake_case-Konsistenz)
- cards/new: Image-Occlusion-Preview zeigt hochgeladenes Bild statt
  statischen Platzhalter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-10 16:12:28 +02:00
parent a883ba87b6
commit b182bac2fb
7 changed files with 64 additions and 67 deletions

View file

@ -0,0 +1,27 @@
import { newReview } from '@cards/domain';
export function makeInitialReviewRows(params: {
userId: string;
cardId: string;
subIndices: number[];
now: Date;
}) {
return params.subIndices.map((subIndex) => {
const r = newReview({ userId: params.userId, cardId: params.cardId, subIndex, now: params.now });
return {
cardId: r.card_id,
subIndex: r.sub_index,
userId: r.user_id,
due: new Date(r.due),
stability: r.stability,
difficulty: r.difficulty,
elapsedDays: r.elapsed_days,
scheduledDays: r.scheduled_days,
learningSteps: r.learning_steps,
reps: r.reps,
lapses: r.lapses,
state: r.state,
lastReview: r.last_review ? new Date(r.last_review) : null,
};
});
}

View file

@ -6,11 +6,12 @@ import {
CardUpdateSchema,
cardContentHash,
maskRegionCount,
newReview,
subIndexCount,
subIndexCountForCloze,
} from '@cards/domain';
import { makeInitialReviewRows } from '../lib/reviews.ts';
import { getDb, type CardsDb } from '../db/connection.ts';
import { cards, decks, reviews } from '../db/schema/index.ts';
import { authMiddleware, type AuthVars } from '../middleware/auth.ts';
@ -100,24 +101,7 @@ export function cardsRouter(deps: CardsDeps = {}): Hono<{ Variables: AuthVars }>
})
.returning();
const initialReviews = subIndices.map((subIndex) => {
const r = newReview({ userId, cardId, subIndex, now });
return {
cardId: r.card_id,
subIndex: r.sub_index,
userId: r.user_id,
due: new Date(r.due),
stability: r.stability,
difficulty: r.difficulty,
elapsedDays: r.elapsed_days,
scheduledDays: r.scheduled_days,
learningSteps: r.learning_steps,
reps: r.reps,
lapses: r.lapses,
state: r.state,
lastReview: r.last_review ? new Date(r.last_review) : null,
};
});
const initialReviews = makeInitialReviewRows({ userId, cardId, subIndices, now });
if (initialReviews.length > 0) {
await tx.insert(reviews).values(initialReviews);
}

View file

@ -2,7 +2,9 @@ import { eq } from 'drizzle-orm';
import { Hono } from 'hono';
import { z } from 'zod';
import { cardContentHash, newReview, subIndexCount } from '@cards/domain';
import { cardContentHash, subIndexCount } from '@cards/domain';
import { makeInitialReviewRows } from '../lib/reviews.ts';
import { getDb, type CardsDb } from '../db/connection.ts';
import { cards, decks, reviews } from '../db/schema/index.ts';
@ -72,24 +74,7 @@ export async function insertGeneratedDeck(
updatedAt: now,
});
const subIndices = Array.from({ length: subIndexCount('basic') }, (_, i) => i);
const initial = subIndices.map((subIndex) => {
const r = newReview({ userId, cardId: cr.id, subIndex, now });
return {
cardId: r.card_id,
subIndex: r.sub_index,
userId: r.user_id,
due: new Date(r.due),
stability: r.stability,
difficulty: r.difficulty,
elapsedDays: r.elapsed_days,
scheduledDays: r.scheduled_days,
learningSteps: r.learning_steps,
reps: r.reps,
lapses: r.lapses,
state: r.state,
lastReview: r.last_review ? new Date(r.last_review) : null,
};
});
const initial = makeInitialReviewRows({ userId, cardId: cr.id, subIndices, now });
await tx.insert(reviews).values(initial);
}
});

View file

@ -123,7 +123,7 @@ export function decksRouter(deps: DecksDeps = {}): Hono<{ Variables: AuthVars }>
r.get('/:deckId/distractors', async (c) => {
const userId = c.get('userId');
const deckId = c.req.param('deckId');
const cardId = c.req.query('cardId') ?? '';
const cardId = c.req.query('card_id') ?? '';
const countRaw = parseInt(c.req.query('count') ?? '3', 10);
const count = isNaN(countRaw) ? 3 : Math.min(10, Math.max(1, countRaw));
const fieldParam = c.req.query('field') ?? 'back';

View file

@ -6,11 +6,12 @@ import {
CardsSearchInputSchema,
cardContentHash,
maskRegionCount,
newReview,
subIndexCount,
subIndexCountForCloze,
} from '@cards/domain';
import { makeInitialReviewRows } from '../lib/reviews.ts';
import { getDb, type CardsDb } from '../db/connection.ts';
import { cards, decks, reviews } from '../db/schema/index.ts';
import { authMiddleware, type AuthVars } from '../middleware/auth.ts';
@ -106,24 +107,8 @@ export function toolsRouter(deps: ToolsDeps = {}): Hono<{ Variables: AuthVars }>
updatedAt: now,
})
.returning();
const initial = Array.from({ length: count }, (_, i) => i).map((subIndex) => {
const r = newReview({ userId, cardId, subIndex, now });
return {
cardId: r.card_id,
subIndex: r.sub_index,
userId: r.user_id,
due: new Date(r.due),
stability: r.stability,
difficulty: r.difficulty,
elapsedDays: r.elapsed_days,
scheduledDays: r.scheduled_days,
learningSteps: r.learning_steps,
reps: r.reps,
lapses: r.lapses,
state: r.state,
lastReview: r.last_review ? new Date(r.last_review) : null,
};
});
const subIndices = Array.from({ length: count }, (_, i) => i);
const initial = makeInitialReviewRows({ userId, cardId, subIndices, now });
if (initial.length > 0) await tx.insert(reviews).values(initial);
return [card];
});