(options?: { force?: boolean })
| 434 | } |
| 435 | |
| 436 | export async function buildIndex(options?: { force?: boolean }): Promise<{ dbPath: string; recordCount: number; newRecords: number }> { |
| 437 | const cachePath = twitterBookmarksCachePath(); |
| 438 | const dbPath = twitterBookmarksIndexPath(); |
| 439 | const records = await readJsonLines<BookmarkRecord>(cachePath); |
| 440 | |
| 441 | const db = await openDb(dbPath); |
| 442 | try { |
| 443 | if (options?.force) { |
| 444 | db.run('DROP TABLE IF EXISTS bookmarks_fts'); |
| 445 | db.run('DROP TABLE IF EXISTS bookmarks'); |
| 446 | db.run('DROP TABLE IF EXISTS meta'); |
| 447 | } |
| 448 | |
| 449 | initSchema(db); |
| 450 | ensureMigrations(db); |
| 451 | |
| 452 | // Preserve classification and enrichment fields when refreshing existing rows. |
| 453 | // Folder fields are normally sourced from JSONL (source of truth) but we also |
| 454 | // preserve them here as defense-in-depth: if a future code path writes folder |
| 455 | // state to the DB without updating JSONL, this keeps it from being wiped on |
| 456 | // the next buildIndex. |
| 457 | const existingRows = new Map<string, PreservedBookmarkFields>(); |
| 458 | try { |
| 459 | const rows = db.exec( |
| 460 | `SELECT id, categories, primary_category, github_urls, domains, primary_domain, |
| 461 | quoted_tweet_json, article_title, article_text, article_site, enriched_at, |
| 462 | folder_ids, folder_names |
| 463 | FROM bookmarks` |
| 464 | ); |
| 465 | for (const r of (rows[0]?.values ?? [])) { |
| 466 | existingRows.set(r[0] as string, { |
| 467 | categories: (r[1] as string) ?? null, |
| 468 | primaryCategory: (r[2] as string) ?? null, |
| 469 | githubUrls: (r[3] as string) ?? null, |
| 470 | domains: (r[4] as string) ?? null, |
| 471 | primaryDomain: (r[5] as string) ?? null, |
| 472 | quotedTweetJson: (r[6] as string) ?? null, |
| 473 | articleTitle: (r[7] as string) ?? null, |
| 474 | articleText: (r[8] as string) ?? null, |
| 475 | articleSite: (r[9] as string) ?? null, |
| 476 | enrichedAt: (r[10] as string) ?? null, |
| 477 | folderIds: (r[11] as string) ?? null, |
| 478 | folderNames: (r[12] as string) ?? null, |
| 479 | }); |
| 480 | } |
| 481 | } catch { /* table may be empty */ } |
| 482 | |
| 483 | const newRecords: BookmarkRecord[] = records.filter(r => !existingRows.has(r.id)); |
| 484 | |
| 485 | if (records.length > 0) { |
| 486 | db.run('BEGIN TRANSACTION'); |
| 487 | try { |
| 488 | for (const record of records) { |
| 489 | insertRecord(db, record, existingRows.get(record.id)); |
| 490 | } |
| 491 | db.run('COMMIT'); |
| 492 | } catch (err) { |
| 493 | db.run('ROLLBACK'); |
no test coverage detected