feat(trash): ImportService deletedAt preservation + skip-merge policy (#4 v0.2.3)
This commit is contained in:
@@ -28,6 +28,7 @@ export interface ImportNoteInput {
|
||||
userIntent: string | null;
|
||||
intentPromptedAt: string | null;
|
||||
tags: { name: string; source: 'ai' | 'user' }[];
|
||||
deletedAt?: string | null;
|
||||
}
|
||||
|
||||
export type ImportNoteStatus = 'inserted' | 'skipped' | 'forked';
|
||||
@@ -294,6 +295,17 @@ export class NoteRepository {
|
||||
let status: ImportNoteStatus = 'inserted';
|
||||
if (existing !== null) {
|
||||
if (existing === input.rawText) {
|
||||
// skip — 단, source 가 deletedAt IS NOT NULL 이고 dest 가 NULL 이면 dest 갱신 (삭제 보존)
|
||||
if (input.deletedAt) {
|
||||
const destRow = this.db
|
||||
.prepare('SELECT deleted_at FROM notes WHERE id=?')
|
||||
.get(input.id) as { deleted_at: string | null } | undefined;
|
||||
if (destRow && destRow.deleted_at === null) {
|
||||
this.db
|
||||
.prepare('UPDATE notes SET deleted_at = ?, updated_at = ? WHERE id = ?')
|
||||
.run(input.deletedAt, input.deletedAt, input.id);
|
||||
}
|
||||
}
|
||||
return { id: input.id, status: 'skipped' };
|
||||
}
|
||||
finalId = uuidv7();
|
||||
@@ -305,8 +317,8 @@ export class NoteRepository {
|
||||
`INSERT INTO notes
|
||||
(id, raw_text, ai_title, ai_summary, ai_status, ai_provider, ai_generated_at,
|
||||
title_edited_by_user, summary_edited_by_user,
|
||||
user_intent, intent_prompted_at, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 'done', ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
user_intent, intent_prompted_at, deleted_at, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 'done', ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
.run(
|
||||
finalId,
|
||||
@@ -319,6 +331,7 @@ export class NoteRepository {
|
||||
input.summaryEditedByUser ? 1 : 0,
|
||||
input.userIntent,
|
||||
input.intentPromptedAt,
|
||||
input.deletedAt ?? null,
|
||||
input.createdAt,
|
||||
input.updatedAt
|
||||
);
|
||||
|
||||
@@ -39,7 +39,8 @@ function parsedToInput(parsed: ParsedNote): ImportNoteInput {
|
||||
aiGeneratedAt: parsed.aiGeneratedAt,
|
||||
userIntent: parsed.userIntent,
|
||||
intentPromptedAt: parsed.intentPromptedAt,
|
||||
tags: parsed.tags
|
||||
tags: parsed.tags,
|
||||
deletedAt: parsed.deletedAt
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface ParsedNote {
|
||||
aiGeneratedAt: string | null;
|
||||
userIntent: string | null;
|
||||
intentPromptedAt: string | null;
|
||||
deletedAt: string | null; // 신규 v0.2.3 #4
|
||||
tags: ParsedNoteTag[];
|
||||
images: ParsedNoteImage[];
|
||||
exportVersion: number;
|
||||
@@ -347,6 +348,7 @@ export function parseExportNote(markdown: string): ParsedNote {
|
||||
aiGeneratedAt: get('ai_generated_at'),
|
||||
userIntent: get('user_intent'),
|
||||
intentPromptedAt: get('intent_prompted_at'),
|
||||
deletedAt: get('deleted_at'),
|
||||
tags: fm.tags,
|
||||
images: fm.images,
|
||||
exportVersion
|
||||
|
||||
@@ -233,3 +233,81 @@ describe('ImportService', () => {
|
||||
expect(dbNote!.media[0]!.bytes).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ImportService — deletedAt preservation (v0.2.3 #4)', () => {
|
||||
it('id-collide skip: source deleted_at IS NOT NULL → dest deleted_at 갱신', () => {
|
||||
const db = new Database(':memory:');
|
||||
runMigrations(db);
|
||||
const repo = new NoteRepository(db);
|
||||
const { id } = repo.create({ rawText: 'identical' });
|
||||
const r = repo.importNote({
|
||||
id, rawText: 'identical',
|
||||
createdAt: '2026-04-01T00:00:00Z', updatedAt: '2026-04-01T00:00:00Z',
|
||||
aiTitle: null, aiSummary: null,
|
||||
titleEditedByUser: false, summaryEditedByUser: false,
|
||||
aiProvider: null, aiGeneratedAt: null,
|
||||
userIntent: null, intentPromptedAt: null,
|
||||
tags: [],
|
||||
deletedAt: '2026-05-01T12:00:00.000Z'
|
||||
});
|
||||
expect(r.status).toBe('skipped');
|
||||
expect(repo.findById(id)!.deletedAt).toBe('2026-05-01T12:00:00.000Z');
|
||||
});
|
||||
|
||||
it('id-collide skip: source deleted_at NULL + dest IS NOT NULL → dest 유지', () => {
|
||||
const db = new Database(':memory:');
|
||||
runMigrations(db);
|
||||
const repo = new NoteRepository(db);
|
||||
const { id } = repo.create({ rawText: 'identical' });
|
||||
repo.trash(id, '2026-05-01T00:00:00.000Z');
|
||||
repo.importNote({
|
||||
id, rawText: 'identical',
|
||||
createdAt: '2026-04-01T00:00:00Z', updatedAt: '2026-04-01T00:00:00Z',
|
||||
aiTitle: null, aiSummary: null,
|
||||
titleEditedByUser: false, summaryEditedByUser: false,
|
||||
aiProvider: null, aiGeneratedAt: null,
|
||||
userIntent: null, intentPromptedAt: null,
|
||||
tags: [],
|
||||
deletedAt: null
|
||||
});
|
||||
expect(repo.findById(id)!.deletedAt).toBe('2026-05-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('id-new insert: source deletedAt 보존', () => {
|
||||
const db = new Database(':memory:');
|
||||
runMigrations(db);
|
||||
const repo = new NoteRepository(db);
|
||||
const r = repo.importNote({
|
||||
id: 'fresh-id', rawText: 'fresh',
|
||||
createdAt: '2026-04-01T00:00:00Z', updatedAt: '2026-04-01T00:00:00Z',
|
||||
aiTitle: null, aiSummary: null,
|
||||
titleEditedByUser: false, summaryEditedByUser: false,
|
||||
aiProvider: null, aiGeneratedAt: null,
|
||||
userIntent: null, intentPromptedAt: null,
|
||||
tags: [],
|
||||
deletedAt: '2026-05-01T12:00:00.000Z'
|
||||
});
|
||||
expect(r.status).toBe('inserted');
|
||||
expect(repo.findById('fresh-id')!.deletedAt).toBe('2026-05-01T12:00:00.000Z');
|
||||
});
|
||||
|
||||
it('id-collide forked: deletedAt 도 fork 노트에 보존', () => {
|
||||
const db = new Database(':memory:');
|
||||
runMigrations(db);
|
||||
const repo = new NoteRepository(db);
|
||||
const { id } = repo.create({ rawText: 'original' });
|
||||
const r = repo.importNote({
|
||||
id, rawText: 'different',
|
||||
createdAt: '2026-04-01T00:00:00Z', updatedAt: '2026-04-01T00:00:00Z',
|
||||
aiTitle: null, aiSummary: null,
|
||||
titleEditedByUser: false, summaryEditedByUser: false,
|
||||
aiProvider: null, aiGeneratedAt: null,
|
||||
userIntent: null, intentPromptedAt: null,
|
||||
tags: [],
|
||||
deletedAt: '2026-05-01T12:00:00.000Z'
|
||||
});
|
||||
expect(r.status).toBe('forked');
|
||||
expect(r.id).not.toBe(id);
|
||||
expect(repo.findById(r.id)!.deletedAt).toBe('2026-05-01T12:00:00.000Z');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user