aac51bf183
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>
92 lines
2.8 KiB
TypeScript
92 lines
2.8 KiB
TypeScript
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
|
|
}
|
|
}
|