From fec0502a2f52de3ab52435222fe0bc57bb1a2091 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:32:01 +0200 Subject: [PATCH] refactor: replace createTables() with Drizzle migrate() Delete ~270 lines of hand-written CREATE TABLE SQL. Schema is now managed entirely by drizzle-kit migrations. initDatabase() flow: baseline check -> migrate() -> ensureDefaults(). --- packages/server/src/db/index.ts | 291 +++----------------------------- 1 file changed, 21 insertions(+), 270 deletions(-) diff --git a/packages/server/src/db/index.ts b/packages/server/src/db/index.ts index 222ce26f..4299bf25 100644 --- a/packages/server/src/db/index.ts +++ b/packages/server/src/db/index.ts @@ -1,11 +1,15 @@ import Database from 'better-sqlite3'; import { drizzle } from 'drizzle-orm/better-sqlite3'; +import { migrate } from 'drizzle-orm/better-sqlite3/migrator'; import { config } from '../config.js'; import * as schema from './schema.js'; -import { runMigrations } from './migrate.js'; +import { baselineExistingInstall, ensureDefaults } from './migrate.js'; import { setWorkerId } from '../utils/snowflake.js'; import { mkdirSync } from 'fs'; -import { dirname } from 'path'; +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); let sqlite: Database.Database; @@ -14,279 +18,26 @@ function ensureDirectory(filePath: string): void { mkdirSync(dir, { recursive: true }); } -function createTables(db: Database.Database): void { - db.exec(` - CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - username TEXT UNIQUE NOT NULL, - display_name TEXT, - password_hash TEXT NOT NULL, - avatar TEXT, - status TEXT DEFAULT 'offline', - custom_status TEXT, - is_admin INTEGER DEFAULT 0, - home_instance TEXT, - home_user_id TEXT, - replicated_instances TEXT DEFAULT '[]', - banner TEXT, - accent_color TEXT, - avatar_color TEXT, - bio TEXT, - is_deleted INTEGER DEFAULT 0, - discoverable INTEGER DEFAULT 1, - profile_updated_at INTEGER, - password_changed_at INTEGER, - created_at INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS spaces ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - icon TEXT, - banner TEXT, - avatar_color TEXT, - owner_id TEXT NOT NULL REFERENCES users(id), - invite_code TEXT UNIQUE, - visibility TEXT DEFAULT 'private', - description TEXT, - created_at INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS space_members ( - space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - nickname TEXT, - joined_at INTEGER NOT NULL, - PRIMARY KEY (space_id, user_id) - ); - - CREATE TABLE IF NOT EXISTS channel_categories ( - id TEXT PRIMARY KEY, - space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE, - name TEXT NOT NULL, - position INTEGER DEFAULT 0, - created_at INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS channels ( - id TEXT PRIMARY KEY, - space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE, - name TEXT NOT NULL, - type TEXT NOT NULL, - topic TEXT, - position INTEGER DEFAULT 0, - category_id TEXT, - created_at INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS messages ( - id TEXT PRIMARY KEY, - channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - user_id TEXT NOT NULL REFERENCES users(id), - reply_to_id TEXT REFERENCES messages(id) ON DELETE SET NULL, - content TEXT, - edited_at INTEGER, - created_at INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS dm_channels ( - id TEXT PRIMARY KEY, - owner_id TEXT, - federated_id TEXT, - owner_home_user_id TEXT, - owner_home_instance TEXT, - deleted_at INTEGER, - created_at INTEGER NOT NULL - ); - -- Note: idx_dm_federated index is created after this block (may fail on pre-migration tables, handled gracefully) - - CREATE TABLE IF NOT EXISTS dm_members ( - dm_channel_id TEXT NOT NULL REFERENCES dm_channels(id) ON DELETE CASCADE, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - closed INTEGER DEFAULT 0, - PRIMARY KEY (dm_channel_id, user_id) - ); - - CREATE TABLE IF NOT EXISTS dm_messages ( - id TEXT PRIMARY KEY, - dm_channel_id TEXT NOT NULL REFERENCES dm_channels(id) ON DELETE CASCADE, - user_id TEXT NOT NULL REFERENCES users(id), - reply_to_id TEXT REFERENCES dm_messages(id) ON DELETE SET NULL, - content TEXT, - edited_at INTEGER, - created_at INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS attachments ( - id TEXT PRIMARY KEY, - message_id TEXT REFERENCES messages(id) ON DELETE CASCADE, - dm_message_id TEXT REFERENCES dm_messages(id) ON DELETE CASCADE, - uploader_id TEXT, - filename TEXT NOT NULL, - original_name TEXT NOT NULL, - mimetype TEXT NOT NULL, - size INTEGER NOT NULL, - thumbnail_filename TEXT, - width INTEGER, - height INTEGER, - duration REAL, - created_at INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS friends ( - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - friend_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - created_at INTEGER NOT NULL, - PRIMARY KEY (user_id, friend_id) - ); - - CREATE TABLE IF NOT EXISTS friend_requests ( - id TEXT PRIMARY KEY, - from_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - to_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - status TEXT DEFAULT 'pending', - created_at INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS reactions ( - id TEXT PRIMARY KEY, - message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - emoji TEXT NOT NULL, - created_at INTEGER NOT NULL, - UNIQUE(message_id, user_id, emoji) - ); - - CREATE TABLE IF NOT EXISTS dm_reactions ( - id TEXT PRIMARY KEY, - dm_message_id TEXT NOT NULL REFERENCES dm_messages(id) ON DELETE CASCADE, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - emoji TEXT NOT NULL, - created_at INTEGER NOT NULL, - UNIQUE(dm_message_id, user_id, emoji) - ); - - CREATE TABLE IF NOT EXISTS roles ( - id TEXT PRIMARY KEY, - space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE, - name TEXT NOT NULL, - color TEXT DEFAULT '#b9bbbe', - position INTEGER DEFAULT 0, - permissions TEXT, - created_at INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS member_roles ( - space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - role_id TEXT NOT NULL REFERENCES roles(id) ON DELETE CASCADE, - PRIMARY KEY (space_id, user_id, role_id) - ); - - CREATE TABLE IF NOT EXISTS channel_overrides ( - channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - target_type TEXT NOT NULL, - target_id TEXT NOT NULL, - allow TEXT NOT NULL DEFAULT '0', - deny TEXT NOT NULL DEFAULT '0', - PRIMARY KEY (channel_id, target_type, target_id) - ); - - CREATE TABLE IF NOT EXISTS read_states ( - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - channel_id TEXT NOT NULL, - last_read_message_id TEXT NOT NULL, - updated_at INTEGER NOT NULL, - PRIMARY KEY (user_id, channel_id) - ); - - CREATE TABLE IF NOT EXISTS space_folders ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - name TEXT, - color TEXT, - position INTEGER DEFAULT 0, - created_at INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS space_folder_members ( - folder_id TEXT NOT NULL REFERENCES space_folders(id) ON DELETE CASCADE, - space_id TEXT NOT NULL, - position INTEGER DEFAULT 0, - PRIMARY KEY (folder_id, space_id) - ); - - CREATE TABLE IF NOT EXISTS user_space_layout ( - user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, - layout TEXT NOT NULL DEFAULT '[]', - updated_at INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS instance_settings ( - id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1), - instance_name TEXT DEFAULT 'Backspace', - worker_id INTEGER, - discovery_enabled INTEGER NOT NULL DEFAULT 1, - max_bitrate_kbps INTEGER NOT NULL DEFAULT 20000, - min_bitrate_kbps INTEGER NOT NULL DEFAULT 500, - bitrate_step_kbps INTEGER NOT NULL DEFAULT 500, - allowed_resolutions TEXT NOT NULL DEFAULT '540,720,1080', - allowed_framerates TEXT NOT NULL DEFAULT '30,45,60', - max_resolution INTEGER NOT NULL DEFAULT 1080, - max_framerate INTEGER NOT NULL DEFAULT 60, - registration_open INTEGER, - max_upload_size_bytes INTEGER, - updated_at INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE IF NOT EXISTS bans ( - space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - reason TEXT, - banned_by TEXT REFERENCES users(id), - created_at INTEGER NOT NULL, - PRIMARY KEY (space_id, user_id) - ); - - CREATE TABLE IF NOT EXISTS join_requests ( - id TEXT PRIMARY KEY, - space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - message TEXT, - status TEXT NOT NULL DEFAULT 'pending', - decided_by TEXT REFERENCES users(id), - created_at INTEGER NOT NULL, - decided_at INTEGER - ); - - CREATE TABLE IF NOT EXISTS voice_restrictions ( - space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - restriction_type TEXT NOT NULL, - moderator_id TEXT REFERENCES users(id), - created_at INTEGER NOT NULL, - PRIMARY KEY (space_id, user_id, restriction_type) - ); - `); - - // Index on federated_id — may fail on existing installs where the column hasn't been - // renamed yet (migration handles it). Safe to skip; migration creates the index. - try { - db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_dm_federated ON dm_channels(federated_id) WHERE federated_id IS NOT NULL`); - } catch { - // Column doesn't exist yet — migration will create the index - } -} - export function initDatabase() { ensureDirectory(config.dbPath); sqlite = new Database(config.dbPath); sqlite.pragma('journal_mode = WAL'); sqlite.pragma('foreign_keys = ON'); - createTables(sqlite); - runMigrations(sqlite); - // Initialize Snowflake worker ID from persisted value (set by migration) + // Baseline existing installs before Drizzle migrate() runs — + // marks the initial migration as applied so it doesn't try to + // CREATE TABLE on a database that already has tables. + baselineExistingInstall(sqlite); + + // Apply any pending Drizzle migrations + const migrationsFolder = resolve(__dirname, '../../drizzle'); + const db = drizzle(sqlite, { schema }); + migrate(db, { migrationsFolder }); + + // Ensure data invariants (settings row, worker ID, first admin) + ensureDefaults(sqlite); + + // Initialize Snowflake worker ID from persisted value const settings = sqlite.prepare('SELECT worker_id FROM instance_settings WHERE id = 1').get() as { worker_id: number } | undefined; if (settings?.worker_id !== undefined && settings.worker_id !== null) { setWorkerId(settings.worker_id); @@ -296,7 +47,7 @@ export function initDatabase() { } console.log(`Database initialized at ${config.dbPath}`); - return drizzle(sqlite, { schema }); + return db; } export type DB = ReturnType;