| 1 | import Database from 'better-sqlite3';
|
|---|
| 2 | import path from 'path';
|
|---|
| 3 | import { fileURLToPath } from 'url';
|
|---|
| 4 | import fs from 'fs';
|
|---|
| 5 |
|
|---|
| 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|---|
| 7 | const dbPath = process.env.DATABASE_PATH || path.join(__dirname, '../../storage/database.sqlite');
|
|---|
| 8 |
|
|---|
| 9 | // Ensure storage directory exists
|
|---|
| 10 | const storageDir = path.dirname(dbPath);
|
|---|
| 11 | if (!fs.existsSync(storageDir)) {
|
|---|
| 12 | fs.mkdirSync(storageDir, { recursive: true });
|
|---|
| 13 | }
|
|---|
| 14 |
|
|---|
| 15 | // Initialize database
|
|---|
| 16 | const db = new Database(dbPath);
|
|---|
| 17 | db.pragma('journal_mode = WAL');
|
|---|
| 18 | db.pragma('foreign_keys = ON');
|
|---|
| 19 |
|
|---|
| 20 | export function initializeDatabase() {
|
|---|
| 21 | const tableExists = db.prepare(`
|
|---|
| 22 | SELECT name FROM sqlite_master WHERE type='table' AND name='users'
|
|---|
| 23 | `).get();
|
|---|
| 24 |
|
|---|
| 25 | if (!tableExists) {
|
|---|
| 26 | console.log('🔧 Initializing database schema...');
|
|---|
| 27 | const schemaPath = path.join(__dirname, '..', 'db', 'migrations', '001-init.sql');
|
|---|
| 28 | const schema = fs.readFileSync(schemaPath, 'utf-8');
|
|---|
| 29 | db.exec(schema);
|
|---|
| 30 | console.log('✅ Database initialized with v9-soul schema');
|
|---|
| 31 | }
|
|---|
| 32 |
|
|---|
| 33 | // Additive column migrations — safe to run every boot.
|
|---|
| 34 | // SQLite throws if the column already exists; we swallow that.
|
|---|
| 35 | ensureColumn('sites', 'enable_audio_player', 'INTEGER DEFAULT 1');
|
|---|
| 36 | ensureColumn('sites', 'profile_photo', 'TEXT');
|
|---|
| 37 | ensureColumn('audio_tracks', 'cover_url', 'TEXT');
|
|---|
| 38 | ensureColumn('audio_tracks', 'album', 'TEXT');
|
|---|
| 39 | ensureColumn('users', 'reset_token', 'TEXT');
|
|---|
| 40 | ensureColumn('users', 'reset_token_expires', 'DATETIME');
|
|---|
| 41 | // Site-level moderation toggle. 'trust' = auto-approve, 'moderate' = pending until reviewed.
|
|---|
| 42 | ensureColumn('sites', 'comments_moderation_mode', "TEXT DEFAULT 'trust'");
|
|---|
| 43 | // Per-site Prutter toggle: when off, DM endpoints/UI are hidden for that site.
|
|---|
| 44 | ensureColumn('sites', 'enable_prutter', 'INTEGER DEFAULT 1');
|
|---|
| 45 |
|
|---|
| 46 | // v9 audit additions —————————————————————————————————————————
|
|---|
| 47 | // SEO/social columns the v9 template uses (most live in 001-init.sql already
|
|---|
| 48 | // for fresh DBs but ensureColumn is idempotent for existing DBs).
|
|---|
| 49 | ensureColumn('sites', 'twitter', 'TEXT'); // @handle (with @)
|
|---|
| 50 | ensureColumn('sites', 'schema_type', "TEXT DEFAULT 'Person'"); // Person|Organization
|
|---|
| 51 | ensureColumn('sites', 'publisher_name', 'TEXT');
|
|---|
| 52 | ensureColumn('sites', 'publisher_url', 'TEXT');
|
|---|
| 53 | ensureColumn('sites', 'publisher_logo', 'TEXT');
|
|---|
| 54 | ensureColumn('sites', 'profile_enabled', 'INTEGER DEFAULT 1');
|
|---|
| 55 | ensureColumn('sites', 'profile_name', 'TEXT'); // display name (falls back to title)
|
|---|
| 56 | ensureColumn('sites', 'profile_bio', 'TEXT'); // short bio for header
|
|---|
| 57 | ensureColumn('sites', 'profile_links', 'TEXT'); // JSON array [{platform, url}]
|
|---|
| 58 | ensureColumn('sites', 'feed_view_default', "TEXT DEFAULT 'timeline'"); // timeline | grid
|
|---|
| 59 | ensureColumn('sites', 'feed_view_switch', 'INTEGER DEFAULT 1'); // show switcher
|
|---|
| 60 | ensureColumn('sites', 'show_search', 'INTEGER DEFAULT 1');
|
|---|
| 61 | ensureColumn('sites', 'show_archive_link', 'INTEGER DEFAULT 1');
|
|---|
| 62 |
|
|---|
| 63 | // Per-post noindex + type
|
|---|
| 64 | ensureColumn('posts', 'noindex', 'INTEGER DEFAULT 0');
|
|---|
| 65 | ensureColumn('posts', 'type', "TEXT DEFAULT 'post'"); // post | foto | video | audio
|
|---|
| 66 |
|
|---|
| 67 | // Playlists (v9 feature) — first-class entity. CREATE IF NOT EXISTS is
|
|---|
| 68 | // idempotent so it's safe to run on every boot regardless of DB age.
|
|---|
| 69 | db.exec(`
|
|---|
| 70 | CREATE TABLE IF NOT EXISTS playlists (
|
|---|
| 71 | id TEXT PRIMARY KEY,
|
|---|
| 72 | site_id TEXT NOT NULL,
|
|---|
| 73 | title TEXT NOT NULL,
|
|---|
| 74 | artist TEXT,
|
|---|
| 75 | year INTEGER,
|
|---|
| 76 | cover_url TEXT,
|
|---|
| 77 | kind TEXT DEFAULT 'album',
|
|---|
| 78 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 79 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 80 | FOREIGN KEY (site_id) REFERENCES sites(id)
|
|---|
| 81 | );
|
|---|
| 82 | CREATE TABLE IF NOT EXISTS playlist_tracks (
|
|---|
| 83 | playlist_id TEXT NOT NULL,
|
|---|
| 84 | track_id TEXT NOT NULL,
|
|---|
| 85 | position INTEGER NOT NULL DEFAULT 0,
|
|---|
| 86 | PRIMARY KEY (playlist_id, track_id),
|
|---|
| 87 | FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
|
|---|
| 88 | FOREIGN KEY (track_id) REFERENCES audio_tracks(id) ON DELETE CASCADE
|
|---|
| 89 | );
|
|---|
| 90 | CREATE INDEX IF NOT EXISTS idx_playlist_tracks_pos
|
|---|
| 91 | ON playlist_tracks(playlist_id, position);
|
|---|
| 92 | `);
|
|---|
| 93 | }
|
|---|
| 94 |
|
|---|
| 95 | function ensureColumn(table, column, definition) {
|
|---|
| 96 | try {
|
|---|
| 97 | db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|---|
| 98 | console.log(`🔧 Added column ${table}.${column}`);
|
|---|
| 99 | } catch (e) {
|
|---|
| 100 | // "duplicate column name" → already there. Anything else, surface it.
|
|---|
| 101 | if (!/duplicate column/i.test(e.message)) {
|
|---|
| 102 | console.error(`❌ ensureColumn(${table}.${column}):`, e.message);
|
|---|
| 103 | }
|
|---|
| 104 | }
|
|---|
| 105 | }
|
|---|
| 106 |
|
|---|
| 107 | export default db;
|
|---|