feat(v0210): NoteRepository revision API + NoteRevision type + InboxApi 시그니처
- updateRawText: raw_text 갱신 + user revision INSERT (atomic) - listRevisions: edited_at DESC 순 hydrate - restoreRevision: 옛 raw_text 를 새 user revision 으로 복원 (chain 보존) - shared/types: NoteRevision + InboxApi 3 메서드 (updateRawText/listRevisions/restoreRevision) - preload: 3 IPC stub 추가 (inbox:update-raw-text / inbox:list-revisions / inbox:restore-revision)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type Database from 'better-sqlite3';
|
||||
import { v7 as uuidv7, v4 as uuidv4 } from 'uuid';
|
||||
import type { AiStatus, Note, NoteMedia, NoteStatus, NoteTag } from '@shared/types';
|
||||
import type { AiStatus, Note, NoteMedia, NoteRevision, NoteStatus, NoteTag } from '@shared/types';
|
||||
import { kstTodayIso } from '../../shared/util/kstDate.js';
|
||||
|
||||
export interface CreateNoteInput {
|
||||
@@ -469,6 +469,69 @@ export class NoteRepository {
|
||||
.run(now, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.2.10 Cut C — 사용자가 raw_text 정정. notes.raw_text 갱신 + note_revisions 에
|
||||
* edited_by='user' 새 row INSERT. 단일 transaction. 호출자 `now` 주입 가능 (테스트성).
|
||||
*
|
||||
* 옛 raw_text 는 backfill (m006) 으로 capture revision 에 이미 보존됨.
|
||||
*/
|
||||
updateRawText(id: string, newText: string, now: Date = new Date()): void {
|
||||
const ts = now.toISOString();
|
||||
const tx = this.db.transaction(() => {
|
||||
this.db
|
||||
.prepare(`UPDATE notes SET raw_text=?, updated_at=? WHERE id=?`)
|
||||
.run(newText, ts, id);
|
||||
this.db
|
||||
.prepare(
|
||||
`INSERT INTO note_revisions (note_id, raw_text, edited_at, edited_by)
|
||||
VALUES (?, ?, ?, 'user')`
|
||||
)
|
||||
.run(id, newText, ts);
|
||||
});
|
||||
tx();
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.2.10 Cut C — 노트의 모든 revision (capture + user) 을 최신순 반환.
|
||||
* NoteCard 의 "이력" modal 에서 사용. edited_at DESC + rev_id DESC tiebreak.
|
||||
*/
|
||||
listRevisions(id: string): NoteRevision[] {
|
||||
const rows = this.db
|
||||
.prepare(
|
||||
`SELECT rev_id, note_id, raw_text, edited_at, edited_by
|
||||
FROM note_revisions
|
||||
WHERE note_id = ?
|
||||
ORDER BY edited_at DESC, rev_id DESC`
|
||||
)
|
||||
.all(id) as Array<{
|
||||
rev_id: number;
|
||||
note_id: string;
|
||||
raw_text: string;
|
||||
edited_at: string;
|
||||
edited_by: 'user' | 'capture';
|
||||
}>;
|
||||
return rows.map((r) => ({
|
||||
revId: r.rev_id,
|
||||
noteId: r.note_id,
|
||||
rawText: r.raw_text,
|
||||
editedAt: r.edited_at,
|
||||
editedBy: r.edited_by
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.2.10 Cut C — 옛 revision 의 raw_text 를 latest 로 복원. chain 끊지 않고
|
||||
* 새 user revision 으로 INSERT (linear history 유지). revId 가 해당 note 의 것이
|
||||
* 아니면 throw — restore 대상 잘못 매칭 방지.
|
||||
*/
|
||||
restoreRevision(id: string, revId: number, now: Date = new Date()): void {
|
||||
const rev = this.db
|
||||
.prepare(`SELECT raw_text FROM note_revisions WHERE rev_id=? AND note_id=?`)
|
||||
.get(revId, id) as { raw_text: string } | undefined;
|
||||
if (!rev) throw new Error(`revision ${revId} not found for note ${id}`);
|
||||
this.updateRawText(id, rev.raw_text, now);
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.2.9 Cut B — 노트 status 4분기 전이 (active/completed/archived/trashed).
|
||||
* status + status_changed_at + move_reason + updated_at 갱신 + deleted_at
|
||||
|
||||
@@ -81,6 +81,10 @@ const api: InklingApi = {
|
||||
// v0.2.9 Cut B Task 16 — disabled 메모 재투입 + count.
|
||||
enqueueDisabled: () => ipcRenderer.invoke('inbox:enqueue-disabled'),
|
||||
getDisabledCount: () => ipcRenderer.invoke('inbox:get-disabled-count'),
|
||||
// v0.2.10 Cut C — raw_text 가변 + revision 보존.
|
||||
updateRawText: (noteId: string, newText: string) => ipcRenderer.invoke('inbox:update-raw-text', noteId, newText),
|
||||
listRevisions: (noteId: string) => ipcRenderer.invoke('inbox:list-revisions', noteId),
|
||||
restoreRevision: (noteId: string, revId: number) => ipcRenderer.invoke('inbox:restore-revision', noteId, revId),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,16 @@ export interface NoteTag {
|
||||
source: 'ai' | 'user';
|
||||
}
|
||||
|
||||
// v0.2.10 Cut C — note_revisions 테이블 row.
|
||||
// 'capture' = 최초 캡처 시점, 'user' = 사용자가 raw_text 정정한 시점.
|
||||
export interface NoteRevision {
|
||||
revId: number;
|
||||
noteId: string;
|
||||
rawText: string;
|
||||
editedAt: string;
|
||||
editedBy: 'user' | 'capture';
|
||||
}
|
||||
|
||||
export interface Note {
|
||||
id: string;
|
||||
rawText: string;
|
||||
@@ -156,6 +166,10 @@ export interface InboxApi {
|
||||
// v0.2.9 Cut B Task 16 — ai_status='disabled' 메모 재투입 (사용자가 ai_enabled OFF→ON 전환 시).
|
||||
enqueueDisabled(): Promise<{ count: number }>;
|
||||
getDisabledCount(): Promise<number>;
|
||||
// v0.2.10 Cut C — raw_text 가변 + revision 보존.
|
||||
updateRawText(noteId: string, newText: string): Promise<{ ok: true } | { ok: false; reason: string }>;
|
||||
listRevisions(noteId: string): Promise<NoteRevision[]>;
|
||||
restoreRevision(noteId: string, revId: number): Promise<{ ok: true } | { ok: false; reason: string }>;
|
||||
}
|
||||
|
||||
export interface InklingApi {
|
||||
|
||||
Reference in New Issue
Block a user