feat: deliver P2 v0.2.0 with unified editor, search, and snapshots
Implement schema v2 migration, outline/setting/inspiration CRUD, unified TipTap editing, reference panel with mentions, FTS global search, chapter version history, writing landmarks, word frequency analysis, light theme contrast fixes, and full unit/E2E test coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import type { InspirationEntry, SettingType } from '../../../shared/types'
|
||||
import type { SqliteDb } from '../types'
|
||||
import { stripHtml } from '../../services/word-count'
|
||||
import type { OutlineRepository } from './outline.repo'
|
||||
import type { SettingRepository } from './setting.repo'
|
||||
|
||||
function mapRow(row: Record<string, unknown>): InspirationEntry {
|
||||
let tags: string[] = []
|
||||
try {
|
||||
tags = JSON.parse((row.tags as string) || '[]') as string[]
|
||||
} catch {
|
||||
tags = []
|
||||
}
|
||||
return {
|
||||
id: row.id as string,
|
||||
title: row.title as string,
|
||||
content: row.content as string,
|
||||
tags,
|
||||
createdAt: row.created_at as string,
|
||||
updatedAt: row.updated_at as string
|
||||
}
|
||||
}
|
||||
|
||||
export class InspirationRepository {
|
||||
constructor(private db: SqliteDb) {}
|
||||
|
||||
create(title = '', content = '', tags: string[] = []): InspirationEntry {
|
||||
const id = randomUUID()
|
||||
this.db
|
||||
.prepare(`INSERT INTO inspiration (id, title, content, tags) VALUES (?, ?, ?, ?)`)
|
||||
.run(id, title, content, JSON.stringify(tags))
|
||||
return this.get(id)!
|
||||
}
|
||||
|
||||
get(id: string): InspirationEntry | null {
|
||||
const row = this.db.prepare('SELECT * FROM inspiration WHERE id = ?').get(id) as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
return row ? mapRow(row) : null
|
||||
}
|
||||
|
||||
list(): InspirationEntry[] {
|
||||
return (
|
||||
this.db.prepare('SELECT * FROM inspiration ORDER BY updated_at DESC').all() as Record<string, unknown>[]
|
||||
).map(mapRow)
|
||||
}
|
||||
|
||||
update(
|
||||
id: string,
|
||||
patch: Partial<{ title: string; content: string; tags: string[] }>
|
||||
): InspirationEntry {
|
||||
const existing = this.get(id)
|
||||
if (!existing) throw new Error('Inspiration not found')
|
||||
|
||||
this.db
|
||||
.prepare(
|
||||
`UPDATE inspiration SET title = ?, content = ?, tags = ?, updated_at = datetime('now') WHERE id = ?`
|
||||
)
|
||||
.run(
|
||||
patch.title ?? existing.title,
|
||||
patch.content ?? existing.content,
|
||||
JSON.stringify(patch.tags ?? existing.tags),
|
||||
id
|
||||
)
|
||||
return this.get(id)!
|
||||
}
|
||||
|
||||
delete(id: string): void {
|
||||
this.db.prepare('DELETE FROM inspiration WHERE id = ?').run(id)
|
||||
}
|
||||
|
||||
convertToOutline(id: string, outlineRepo: OutlineRepository) {
|
||||
const item = this.get(id)
|
||||
if (!item) throw new Error('Inspiration not found')
|
||||
|
||||
const outline = outlineRepo.create(null, item.title || '未命名灵感', outlineRepo.nextSortOrder(null))
|
||||
outlineRepo.update(outline.id, { description: stripHtml(item.content) })
|
||||
this.delete(id)
|
||||
return outlineRepo.get(outline.id)!
|
||||
}
|
||||
|
||||
convertToSetting(id: string, type: SettingType, settingRepo: SettingRepository) {
|
||||
const item = this.get(id)
|
||||
if (!item) throw new Error('Inspiration not found')
|
||||
|
||||
const setting = settingRepo.create(type, item.title || '未命名设定', stripHtml(item.content))
|
||||
this.delete(id)
|
||||
return setting
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user