diff --git a/src/main/repository/NoteRepository.ts b/src/main/repository/NoteRepository.ts index c67029f..36add73 100644 --- a/src/main/repository/NoteRepository.ts +++ b/src/main/repository/NoteRepository.ts @@ -238,6 +238,34 @@ export class NoteRepository { .run(now, id); } + permanentDelete(id: string): void { + this.db.prepare('DELETE FROM notes WHERE id=?').run(id); + } + + emptyTrash(): { noteIds: string[] } { + const noteIds: string[] = []; + const tx = this.db.transaction(() => { + const rows = this.db + .prepare('SELECT id FROM notes WHERE deleted_at IS NOT NULL') + .all() as Array<{ id: string }>; + for (const r of rows) { + this.db.prepare('DELETE FROM notes WHERE id=?').run(r.id); + noteIds.push(r.id); + } + }); + tx(); + return { noteIds }; + } + + listTrashed(opts: { limit: number }): Note[] { + const limit = Math.max(1, Math.min(200, opts.limit)); + const rows = this.db + .prepare(`SELECT * FROM notes WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC, id DESC LIMIT ?`) + .all(limit) as any[]; + return rows.map((r) => this.hydrate(r)); + } + + /** @deprecated v0.2.3 #4 부터 hard delete 는 permanentDelete() 사용. soft delete 는 trash(). 본 메서드는 v0.2.4 에서 제거 예정. */ delete(id: string): void { this.db.prepare('DELETE FROM notes WHERE id=?').run(id); } diff --git a/tests/unit/NoteRepository.test.ts b/tests/unit/NoteRepository.test.ts index 3c7042a..1099b55 100644 --- a/tests/unit/NoteRepository.test.ts +++ b/tests/unit/NoteRepository.test.ts @@ -280,3 +280,86 @@ describe('NoteRepository.restore', () => { expect(repo.findById(id)!.deletedAt).toBeNull(); }); }); + +describe('NoteRepository.permanentDelete', () => { + let db: Database.Database; + let repo: NoteRepository; + beforeEach(() => { + db = new Database(':memory:'); + runMigrations(db); + repo = new NoteRepository(db); + }); + + it('removes notes row + cascades note_tags / pending_jobs', () => { + const { id } = repo.create({ rawText: 'x' }); + repo.updateAiResult(id, { title: 'T', summary: 'a\nb\nc', tags: ['tag-a'], provider: 'p', dueDate: null }); + expect(db.prepare('SELECT COUNT(*) AS c FROM note_tags WHERE note_id=?').get(id)).toMatchObject({ c: 1 }); + repo.permanentDelete(id); + expect(repo.findById(id)).toBeNull(); + expect(db.prepare('SELECT COUNT(*) AS c FROM note_tags WHERE note_id=?').get(id)).toMatchObject({ c: 0 }); + expect(db.prepare('SELECT COUNT(*) AS c FROM pending_jobs WHERE note_id=?').get(id)).toMatchObject({ c: 0 }); + }); +}); + +describe('NoteRepository.emptyTrash', () => { + let db: Database.Database; + let repo: NoteRepository; + beforeEach(() => { + db = new Database(':memory:'); + runMigrations(db); + repo = new NoteRepository(db); + }); + + it('hard-deletes all trashed notes and returns their ids', () => { + const a = repo.create({ rawText: 'a' }).id; + const b = repo.create({ rawText: 'b' }).id; + const c = repo.create({ rawText: 'c' }).id; + repo.trash(a, '2026-05-01T00:00:00.000Z'); + repo.trash(c, '2026-05-01T01:00:00.000Z'); + const r = repo.emptyTrash(); + expect(r.noteIds.sort()).toEqual([a, c].sort()); + expect(repo.findById(a)).toBeNull(); + expect(repo.findById(b)).not.toBeNull(); + expect(repo.findById(c)).toBeNull(); + }); + + it('returns empty array when trash is empty', () => { + expect(repo.emptyTrash()).toEqual({ noteIds: [] }); + }); +}); + +describe('NoteRepository.listTrashed', () => { + let db: Database.Database; + let repo: NoteRepository; + beforeEach(() => { + db = new Database(':memory:'); + runMigrations(db); + repo = new NoteRepository(db); + }); + + it('returns trashed notes ordered by deleted_at DESC', () => { + const a = repo.create({ rawText: 'a' }).id; + const b = repo.create({ rawText: 'b' }).id; + const c = repo.create({ rawText: 'c' }).id; + repo.trash(a, '2026-05-01T00:00:00.000Z'); + repo.trash(c, '2026-05-01T02:00:00.000Z'); + repo.trash(b, '2026-05-01T01:00:00.000Z'); + const r = repo.listTrashed({ limit: 50 }); + expect(r.map((n) => n.id)).toEqual([c, b, a]); + }); + + it('excludes active notes', () => { + repo.create({ rawText: 'active' }); + const r = repo.listTrashed({ limit: 50 }); + expect(r).toEqual([]); + }); + + it('respects limit', () => { + for (let i = 0; i < 5; i++) { + const id = repo.create({ rawText: `n${i}` }).id; + repo.trash(id, `2026-05-01T0${i}:00:00.000Z`); + } + const r = repo.listTrashed({ limit: 3 }); + expect(r).toHaveLength(3); + }); +});