From b074a4901c630ee5c5f7dcff79fa6ff911a14ded Mon Sep 17 00:00:00 2001 From: nsfisis Date: Wed, 31 Dec 2025 14:19:22 +0900 Subject: feat(schema): make note_id and is_reversed NOT NULL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All cards now require note association - legacy card support removed. This aligns with the note-based card architecture introduced in Phase 8. - Add database migration for NOT NULL constraints - Update client Dexie schema to version 3 - Remove LegacyCardItem component and legacy card handling - Update sync schemas and type definitions - Update all tests to use note-based cards 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/server/repositories/card.test.ts | 69 ++++++++++++++++-------------------- src/server/repositories/card.ts | 56 ++++++++--------------------- src/server/repositories/note.test.ts | 4 +-- src/server/repositories/sync.ts | 4 +-- src/server/repositories/types.ts | 14 ++++---- 5 files changed, 56 insertions(+), 91 deletions(-) (limited to 'src/server/repositories') diff --git a/src/server/repositories/card.test.ts b/src/server/repositories/card.test.ts index 9d7ffa6..98913e9 100644 --- a/src/server/repositories/card.test.ts +++ b/src/server/repositories/card.test.ts @@ -12,8 +12,8 @@ function createMockCard(overrides: Partial = {}): Card { return { id: "card-uuid-123", deckId: "deck-uuid-123", - noteId: null, - isReversed: null, + noteId: "note-uuid-123", + isReversed: false, front: "Front text", back: "Back text", state: 0, @@ -89,15 +89,14 @@ function createMockCardWithNoteData( function createMockCardForStudy( overrides: Partial = {}, ): CardForStudy { - const card = createMockCard({ - noteId: overrides.noteType ? "note-uuid-123" : null, - isReversed: overrides.noteType ? false : null, - ...overrides, - }); + const card = createMockCard(overrides); return { ...card, - noteType: overrides.noteType ?? null, - fieldValuesMap: overrides.fieldValuesMap ?? {}, + noteType: overrides.noteType ?? { + frontTemplate: "{{Front}}", + backTemplate: "{{Back}}", + }, + fieldValuesMap: overrides.fieldValuesMap ?? { Front: "Q", Back: "A" }, }; } @@ -125,8 +124,8 @@ describe("CardRepository mock factory", () => { expect(card.id).toBe("card-uuid-123"); expect(card.deckId).toBe("deck-uuid-123"); - expect(card.noteId).toBeNull(); - expect(card.isReversed).toBeNull(); + expect(card.noteId).toBe("note-uuid-123"); + expect(card.isReversed).toBe(false); expect(card.front).toBe("Front text"); expect(card.back).toBe("Back text"); expect(card.state).toBe(0); @@ -205,17 +204,13 @@ describe("CardRepository mock factory", () => { expect(cardWithNote.fieldValues[0]?.value).toBe("日本語"); }); - it("can represent legacy card with null note", () => { - // For legacy cards without notes, we explicitly construct the type - const legacyCard: CardWithNoteData = { - ...createMockCard({ noteId: null, isReversed: null }), - note: null, - fieldValues: [], - }; - - expect(legacyCard.noteId).toBeNull(); - expect(legacyCard.note).toBeNull(); - expect(legacyCard.fieldValues).toHaveLength(0); + it("card always has note association", () => { + // All cards now require note association + const cardWithNote = createMockCardWithNoteData(); + + expect(cardWithNote.noteId).toBe("note-uuid-123"); + expect(cardWithNote.note).not.toBeNull(); + expect(cardWithNote.fieldValues).toHaveLength(2); }); }); @@ -392,41 +387,39 @@ describe("Card interface contracts", () => { expect(cardForStudy.fieldValuesMap.Front).toBe("Question"); }); - it("CardForStudy can represent legacy card with null noteType", () => { - const legacyCard = createMockCardForStudy({ - front: "Legacy Question", - back: "Legacy Answer", + it("CardForStudy has required note data", () => { + const cardForStudy = createMockCardForStudy({ + front: "Question", + back: "Answer", }); - expect(legacyCard.noteId).toBeNull(); - expect(legacyCard.noteType).toBeNull(); - expect(legacyCard.fieldValuesMap).toEqual({}); - expect(legacyCard.front).toBe("Legacy Question"); - expect(legacyCard.back).toBe("Legacy Answer"); + expect(cardForStudy.noteId).toBe("note-uuid-123"); + expect(cardForStudy.noteType).not.toBeNull(); + expect(cardForStudy.noteType.frontTemplate).toBe("{{Front}}"); + expect(cardForStudy.fieldValuesMap).toEqual({ Front: "Q", Back: "A" }); }); }); describe("Card and Note relationship", () => { - it("legacy card has null noteId and isReversed", () => { + it("card has required noteId and isReversed", () => { const card = createMockCard(); - expect(card.noteId).toBeNull(); - expect(card.isReversed).toBeNull(); + expect(card.noteId).toBe("note-uuid-123"); + expect(card.isReversed).toBe(false); }); - it("note-based card has noteId and isReversed set", () => { + it("card with explicit note data", () => { const card = createMockCard({ - noteId: "note-uuid-123", + noteId: "different-note-id", isReversed: false, }); - expect(card.noteId).toBe("note-uuid-123"); + expect(card.noteId).toBe("different-note-id"); expect(card.isReversed).toBe(false); }); it("reversed card has isReversed true", () => { const card = createMockCard({ - noteId: "note-uuid-123", isReversed: true, }); diff --git a/src/server/repositories/card.ts b/src/server/repositories/card.ts index 7116642..4c4fc81 100644 --- a/src/server/repositories/card.ts +++ b/src/server/repositories/card.ts @@ -47,20 +47,15 @@ export const cardRepository: CardRepository = { return undefined; } - if (!card.noteId) { - return { - ...card, - note: null, - fieldValues: [], - }; - } - const noteResult = await db .select() .from(notes) .where(and(eq(notes.id, card.noteId), isNull(notes.deletedAt))); - const note = noteResult[0] ?? null; + const note = noteResult[0]; + if (!note) { + return undefined; + } const fieldValuesResult = await db .select() @@ -85,6 +80,8 @@ export const cardRepository: CardRepository = { async create( deckId: string, data: { + noteId: string; + isReversed: boolean; front: string; back: string; }, @@ -93,6 +90,8 @@ export const cardRepository: CardRepository = { .insert(cards) .values({ deckId, + noteId: data.noteId, + isReversed: data.isReversed, front: data.front, back: data.back, state: CardState.New, @@ -200,21 +199,16 @@ export const cardRepository: CardRepository = { const cardsWithNoteData: CardWithNoteData[] = []; for (const card of dueCards) { - if (!card.noteId) { - cardsWithNoteData.push({ - ...card, - note: null, - fieldValues: [], - }); - continue; - } - const noteResult = await db .select() .from(notes) .where(and(eq(notes.id, card.noteId), isNull(notes.deletedAt))); - const note = noteResult[0] ?? null; + const note = noteResult[0]; + if (!note) { + // Note was deleted, skip this card + continue; + } const fieldValuesResult = await db .select() @@ -241,16 +235,6 @@ export const cardRepository: CardRepository = { const cardsForStudy: CardForStudy[] = []; for (const card of dueCards) { - // Legacy card (no note association) - if (!card.noteId) { - cardsForStudy.push({ - ...card, - noteType: null, - fieldValuesMap: {}, - }); - continue; - } - // Fetch note to get noteTypeId const noteResult = await db .select() @@ -259,12 +243,7 @@ export const cardRepository: CardRepository = { const note = noteResult[0]; if (!note) { - // Note was deleted, treat as legacy card - cardsForStudy.push({ - ...card, - noteType: null, - fieldValuesMap: {}, - }); + // Note was deleted, skip this card continue; } @@ -281,12 +260,7 @@ export const cardRepository: CardRepository = { const noteType = noteTypeResult[0]; if (!noteType) { - // Note type was deleted, treat as legacy card - cardsForStudy.push({ - ...card, - noteType: null, - fieldValuesMap: {}, - }); + // Note type was deleted, skip this card continue; } diff --git a/src/server/repositories/note.test.ts b/src/server/repositories/note.test.ts index 70dcb4a..cc1a9ae 100644 --- a/src/server/repositories/note.test.ts +++ b/src/server/repositories/note.test.ts @@ -40,8 +40,8 @@ function createMockCard(overrides: Partial = {}): Card { return { id: "card-uuid-123", deckId: "deck-uuid-123", - noteId: null, - isReversed: null, + noteId: "note-uuid-123", + isReversed: false, front: "Front text", back: "Back text", state: 0, diff --git a/src/server/repositories/sync.ts b/src/server/repositories/sync.ts index 8c4fd25..59a195a 100644 --- a/src/server/repositories/sync.ts +++ b/src/server/repositories/sync.ts @@ -45,8 +45,8 @@ export interface SyncDeckData { export interface SyncCardData { id: string; deckId: string; - noteId: string | null; - isReversed: boolean | null; + noteId: string; + isReversed: boolean; front: string; back: string; state: number; diff --git a/src/server/repositories/types.ts b/src/server/repositories/types.ts index c864be0..a986cad 100644 --- a/src/server/repositories/types.ts +++ b/src/server/repositories/types.ts @@ -81,8 +81,8 @@ export interface DeckRepository { export interface Card { id: string; deckId: string; - noteId: string | null; - isReversed: boolean | null; + noteId: string; + isReversed: boolean; front: string; back: string; @@ -104,22 +104,20 @@ export interface Card { } export interface CardWithNoteData extends Card { - note: Note | null; + note: Note; fieldValues: NoteFieldValue[]; } /** * Card data prepared for study, including all necessary template rendering info. - * For note-based cards, includes templates and field values as a name-value map. - * For legacy cards, note and templates are null. */ export interface CardForStudy extends Card { - /** Note type templates for rendering (null for legacy cards) */ + /** Note type templates for rendering */ noteType: { frontTemplate: string; backTemplate: string; - } | null; - /** Field values as a name-value map for template rendering (empty for legacy cards) */ + }; + /** Field values as a name-value map for template rendering */ fieldValuesMap: Record; } -- cgit v1.2.3-70-g09d2