From 5bcfd26bfdc50bbb44176ca3b08e08562cc3d575 Mon Sep 17 00:00:00 2001 From: altair823 Date: Fri, 1 May 2026 20:32:52 +0900 Subject: [PATCH] feat(trash): migration v3 + Note type extension (#4 v0.2.3) Co-Authored-By: Claude Sonnet 4.6 --- src/main/db/migrations/index.ts | 3 +- src/main/db/migrations/m003_soft_delete.ts | 12 ++++++ src/main/repository/NoteRepository.ts | 3 ++ src/shared/types.ts | 4 ++ tests/unit/migrations.test.ts | 44 ++++++++++++++++++++++ tests/unit/store.tagFilter.test.ts | 3 ++ 6 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/main/db/migrations/m003_soft_delete.ts diff --git a/src/main/db/migrations/index.ts b/src/main/db/migrations/index.ts index 0ab8250..61129bd 100644 --- a/src/main/db/migrations/index.ts +++ b/src/main/db/migrations/index.ts @@ -1,8 +1,9 @@ import type Database from 'better-sqlite3'; import * as m001 from './m001_initial.js'; import * as m002 from './m002_due_date.js'; +import * as m003 from './m003_soft_delete.js'; -const migrations = [m001, m002]; +const migrations = [m001, m002, m003]; export function latestVersion(): number { return migrations[migrations.length - 1]!.version; diff --git a/src/main/db/migrations/m003_soft_delete.ts b/src/main/db/migrations/m003_soft_delete.ts new file mode 100644 index 0000000..590ea00 --- /dev/null +++ b/src/main/db/migrations/m003_soft_delete.ts @@ -0,0 +1,12 @@ +import type Database from 'better-sqlite3'; + +export const version = 3; + +export function up(db: Database.Database): void { + db.exec(` + ALTER TABLE notes ADD COLUMN deleted_at TEXT; + ALTER TABLE notes ADD COLUMN last_recalled_at TEXT; + ALTER TABLE notes ADD COLUMN recall_dismissed_at TEXT; + CREATE INDEX IF NOT EXISTS idx_notes_deleted_at ON notes(deleted_at); + `); +} diff --git a/src/main/repository/NoteRepository.ts b/src/main/repository/NoteRepository.ts index 13d26cb..8541b60 100644 --- a/src/main/repository/NoteRepository.ts +++ b/src/main/repository/NoteRepository.ts @@ -375,6 +375,9 @@ export class NoteRepository { intentPromptedAt: row.intent_prompted_at, dueDate: row.due_date ?? null, dueDateEditedByUser: row.due_date_edited_by_user === 1, + deletedAt: row.deleted_at ?? null, + lastRecalledAt: row.last_recalled_at ?? null, + recallDismissedAt: row.recall_dismissed_at ?? null, createdAt: row.created_at, updatedAt: row.updated_at, tags: tags as NoteTag[], diff --git a/src/shared/types.ts b/src/shared/types.ts index af566e9..b968901 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -33,6 +33,10 @@ export interface Note { intentPromptedAt: string | null; dueDate: string | null; dueDateEditedByUser: boolean; + // 신규 v3: + deletedAt: string | null; + lastRecalledAt: string | null; + recallDismissedAt: string | null; createdAt: string; updatedAt: string; tags: NoteTag[]; diff --git a/tests/unit/migrations.test.ts b/tests/unit/migrations.test.ts index 11d4e51..b4b66a8 100644 --- a/tests/unit/migrations.test.ts +++ b/tests/unit/migrations.test.ts @@ -29,3 +29,47 @@ describe('migrations', () => { db.close(); }); }); + +describe('migration v3 — soft delete columns', () => { + it('adds deleted_at, last_recalled_at, recall_dismissed_at to notes', () => { + const db = new Database(':memory:'); + runMigrations(db); + const cols = db.prepare(`PRAGMA table_info(notes)`).all().map((r: any) => r.name); + expect(cols).toEqual( + expect.arrayContaining(['deleted_at', 'last_recalled_at', 'recall_dismissed_at']) + ); + db.close(); + }); + + it('creates idx_notes_deleted_at index', () => { + const db = new Database(':memory:'); + runMigrations(db); + const indexes = db + .prepare(`SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='notes'`) + .all() as Array<{ name: string }>; + expect(indexes.map((i) => i.name)).toContain('idx_notes_deleted_at'); + db.close(); + }); + + it('user_version reaches 3', () => { + const db = new Database(':memory:'); + runMigrations(db); + const row = db.prepare('PRAGMA user_version').get() as { user_version: number }; + expect(row.user_version).toBe(3); + db.close(); + }); + + it('all 3 new columns default to NULL', () => { + const db = new Database(':memory:'); + runMigrations(db); + db.prepare( + `INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at) + VALUES ('n1', 't', 'pending', '2026-05-01T00:00:00Z', '2026-05-01T00:00:00Z')` + ).run(); + const row = db.prepare('SELECT deleted_at, last_recalled_at, recall_dismissed_at FROM notes WHERE id=?').get('n1') as any; + expect(row.deleted_at).toBeNull(); + expect(row.last_recalled_at).toBeNull(); + expect(row.recall_dismissed_at).toBeNull(); + db.close(); + }); +}); diff --git a/tests/unit/store.tagFilter.test.ts b/tests/unit/store.tagFilter.test.ts index e4f6765..66dbbb5 100644 --- a/tests/unit/store.tagFilter.test.ts +++ b/tests/unit/store.tagFilter.test.ts @@ -18,6 +18,9 @@ function sample(id: string, tags: string[]): Note { intentPromptedAt: null, dueDate: null, dueDateEditedByUser: false, + deletedAt: null, + lastRecalledAt: null, + recallDismissedAt: null, createdAt: '2026-04-26T00:00:00Z', updatedAt: '2026-04-26T00:00:00Z', tags: tags.map((name) => ({ name, source: 'ai' as const })),