feat(trash): NoteRepository.permanentDelete/emptyTrash/listTrashed (#4 v0.2.3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user