Files
VidBee/src/main/lib/database/migrate.ts
Nexmoe 62a339e5b9 feat: rss-subscriptions (#28)
* feat(subscriptions): add subscription atoms and types for feeds and rules

* refactor(types): remove obsolete format/quality/codec fields and related logic to simplify download and avoid redundant storage

* feat: add download_history schema, migration, and tag utils

* refactor(dialog): convert to functional wrappers, simplify overlay and

* feat(rss): add enclosure support, refine thumbnail lookup and UI labels
feat(settings): add toggleable anonymous analytics collection and loader script

* feat(settings): add toggleable anonymous analytics collection and loader script

* feat(i18n): add subscriptions UI strings and import downloads types

* refactor(subscriptions): remove legacy status fields and migrate items ordering

* feat: add Radix HoverCard dependency and wrap tabs with HoverCard for

* feat(db): move download history schema into migrate + drizzle config

* feat(subscriptions): add item queueing UI and IPC handler

* feat(downloads): normalize saved filenames, improve candidates and UI display
2025-11-15 12:29:31 +08:00

98 lines
3.0 KiB
TypeScript

import { existsSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { sql } from 'drizzle-orm'
import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
import { type MigrationMeta, readMigrationFiles } from 'drizzle-orm/migrator'
import { app } from 'electron'
import log from 'electron-log/main'
const MIGRATIONS_RELATIVE_PATH = 'resources/drizzle'
const MIGRATIONS_TABLE = '__drizzle_migrations'
const KNOWN_TABLES = ['download_history', 'subscriptions', 'subscription_items']
const migrationsTableDefinition = sql`
CREATE TABLE IF NOT EXISTS ${sql.identifier(MIGRATIONS_TABLE)} (
id SERIAL PRIMARY KEY,
hash text NOT NULL,
created_at numeric
)
`
export const runMigrations = (database: BetterSQLite3Database): void => {
const migrationsFolder = resolveMigrationsFolder()
if (!migrationsFolder) {
log.warn('database: drizzle migrations folder not found, skipping migrations')
return
}
try {
ensureBaseline(database, migrationsFolder)
migrate(database, { migrationsFolder, migrationsTable: MIGRATIONS_TABLE })
} catch (error) {
log.error('database: failed to run drizzle migrations', error)
}
}
const resolveMigrationsFolder = (): string | null => {
const candidates = new Set<string>()
candidates.add(resolve(process.cwd(), MIGRATIONS_RELATIVE_PATH))
candidates.add(resolve(__dirname, '../../../../', MIGRATIONS_RELATIVE_PATH))
if (process.resourcesPath) {
candidates.add(join(process.resourcesPath, MIGRATIONS_RELATIVE_PATH))
candidates.add(join(process.resourcesPath, 'app.asar.unpacked', MIGRATIONS_RELATIVE_PATH))
}
try {
candidates.add(join(app.getAppPath(), MIGRATIONS_RELATIVE_PATH))
} catch {
// app might not be ready yet, ignore
}
for (const candidate of candidates) {
if (existsSync(candidate)) {
return candidate
}
}
return null
}
const ensureBaseline = (database: BetterSQLite3Database, migrationsFolder: string): void => {
if (hasTable(database, MIGRATIONS_TABLE)) {
return
}
const hasExistingSchema = KNOWN_TABLES.some((table) => hasTable(database, table))
if (!hasExistingSchema) {
return
}
log.info('database: detected existing schema, seeding drizzle migrations baseline')
database.run(migrationsTableDefinition)
let migrations: MigrationMeta[]
try {
migrations = readMigrationFiles({ migrationsFolder, migrationsTable: MIGRATIONS_TABLE })
} catch (error) {
log.error('database: failed to read drizzle migrations while seeding baseline', error)
return
}
database.transaction((tx) => {
for (const migration of migrations) {
tx.run(
sql`INSERT INTO ${sql.identifier(MIGRATIONS_TABLE)} ("hash", "created_at") VALUES(${migration.hash}, ${migration.folderMillis})`
)
}
})
}
const hasTable = (database: BetterSQLite3Database, tableName: string): boolean => {
const rows = database.all<{ name: string }>(
sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${tableName}`
)
return rows.length > 0
}