- tsconfig.base.json: allowImportingTsExtensions + noEmit (.ts-Imports
in dev, kein tsc-Output, vitest/bun/vite handhaben Build)
- ts-fsrs v5.3.2 API-Updates:
- scheduler.next(card, now, grade) statt repeat(card, now)[rating].card
- Grade-Type für RATING_TO_FSRS (excluded Manual)
- learning_steps-Feld auf Review (Schema, Drizzle-Column, Adapter,
DTO-Konverter, Tests)
- apps/web: extends .svelte-kit/tsconfig.json (SvelteKit-Empfehlung),
test-Script mit --passWithNoTests
- apps/api: dropped types: ['bun-types'] (stale)
- pnpm-lock.yaml committed
Status:
- pnpm run type-check ✅ 4/4 packages grün (api, domain, web mit
svelte-check 0 errors)
- pnpm run test ✅ 46 Tests grün (cards-domain: 27, apps/api: 19,
apps/web: --passWithNoTests)
- pnpm install ✅ 136 packages, 8s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
import { index, integer, primaryKey, real, text, timestamp } from 'drizzle-orm/pg-core';
|
|
|
|
import { cardsSchema } from './_schema.ts';
|
|
import { cards } from './cards.ts';
|
|
import { decks } from './decks.ts';
|
|
|
|
/**
|
|
* FSRS-Review-State pro `(card, sub_index)`.
|
|
*
|
|
* `sub_index` granular:
|
|
* - basic: 1 Review (sub_index = 0)
|
|
* - basic-reverse: 2 Reviews (0 = front→back, 1 = back→front)
|
|
* - cloze: 1 Review pro Cluster-Index ({{c1::…}} → sub_index = 1)
|
|
*
|
|
* **Bewusst PLAINTEXT** (siehe `docs/LESSONS_FROM_MANA_MONOREPO.md` §3):
|
|
* Der Scheduler quert täglich `due <= now` — Encryption müsste das
|
|
* jedes Mal entschlüsseln. mana-monorepo hat das gleiche Pattern:
|
|
* cardReviews ist plaintext-allowlisted.
|
|
*/
|
|
export const reviews = cardsSchema.table(
|
|
'reviews',
|
|
{
|
|
cardId: text('card_id')
|
|
.notNull()
|
|
.references(() => cards.id, { onDelete: 'cascade' }),
|
|
subIndex: integer('sub_index').notNull().default(0),
|
|
userId: text('user_id').notNull(),
|
|
due: timestamp('due', { withTimezone: true, mode: 'date' }).notNull(),
|
|
stability: real('stability').notNull(),
|
|
difficulty: real('difficulty').notNull(),
|
|
elapsedDays: real('elapsed_days').notNull().default(0),
|
|
scheduledDays: real('scheduled_days').notNull().default(0),
|
|
learningSteps: integer('learning_steps').notNull().default(0),
|
|
reps: integer('reps').notNull().default(0),
|
|
lapses: integer('lapses').notNull().default(0),
|
|
state: text('state', { enum: ['new', 'learning', 'review', 'relearning'] })
|
|
.notNull()
|
|
.default('new'),
|
|
lastReview: timestamp('last_review', { withTimezone: true, mode: 'date' }),
|
|
},
|
|
(t) => ({
|
|
pk: primaryKey({ columns: [t.cardId, t.subIndex] }),
|
|
// Hot Path: Scheduler quert täglich `due <= now` für einen User.
|
|
userDueIdx: index('reviews_user_due_idx').on(t.userId, t.due),
|
|
})
|
|
);
|
|
|
|
export type ReviewRow = typeof reviews.$inferSelect;
|
|
export type ReviewInsert = typeof reviews.$inferInsert;
|
|
|
|
/**
|
|
* Study-Sessions als Statistik-Layer. Eine Session läuft pro
|
|
* `(user, deck)`-Studieren-Lauf, wird beim Start angelegt und beim
|
|
* Ende mit Total-Counts geupdatet.
|
|
*/
|
|
export const studySessions = cardsSchema.table(
|
|
'study_sessions',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
userId: text('user_id').notNull(),
|
|
deckId: text('deck_id')
|
|
.notNull()
|
|
.references(() => decks.id, { onDelete: 'cascade' }),
|
|
startedAt: timestamp('started_at', { withTimezone: true, mode: 'date' })
|
|
.notNull()
|
|
.defaultNow(),
|
|
finishedAt: timestamp('finished_at', { withTimezone: true, mode: 'date' }),
|
|
cardsReviewed: integer('cards_reviewed').notNull().default(0),
|
|
cardsCorrect: integer('cards_correct').notNull().default(0),
|
|
},
|
|
(t) => ({
|
|
userStartedIdx: index('sessions_user_started_idx').on(t.userId, t.startedAt),
|
|
})
|
|
);
|
|
|
|
export type StudySessionRow = typeof studySessions.$inferSelect;
|
|
export type StudySessionInsert = typeof studySessions.$inferInsert;
|