// Package database handles SQLite database operations package database import ( "database/sql" "fmt" "os" "path/filepath" _ "modernc.org/sqlite" ) // Open opens the SQLite database with WAL mode enabled func Open(path string) (*sql.DB, error) { // Ensure directory exists dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return nil, fmt.Errorf("create database directory: %w", err) } // Open database with WAL mode and busy timeout dsn := fmt.Sprintf("%s?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=foreign_keys(ON)", path) db, err := sql.Open("sqlite", dsn) if err != nil { return nil, fmt.Errorf("open database: %w", err) } // Test connection if err := db.Ping(); err != nil { db.Close() return nil, fmt.Errorf("ping database: %w", err) } // Set connection pool settings for SQLite db.SetMaxOpenConns(1) // SQLite single writer db.SetMaxIdleConns(1) return db, nil } // Migrate runs all database migrations func Migrate(db *sql.DB) error { migrations := []string{ migrationDevelopers, migrationAPIKeys, migrationApps, migrationAppVersions, migrationSigningKeys, migrationTelemetry, migrationAuditLogs, migrationIndexes, } for i, migration := range migrations { if _, err := db.Exec(migration); err != nil { return fmt.Errorf("migration %d: %w", i+1, err) } } return nil } const migrationDevelopers = ` CREATE TABLE IF NOT EXISTS developers ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, name TEXT NOT NULL, password_hash TEXT, oauth_provider TEXT, oauth_id TEXT, verified INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); ` const migrationAPIKeys = ` CREATE TABLE IF NOT EXISTS api_keys ( id TEXT PRIMARY KEY, developer_id TEXT NOT NULL REFERENCES developers(id) ON DELETE CASCADE, name TEXT NOT NULL, key_hash TEXT NOT NULL, key_prefix TEXT NOT NULL, permissions TEXT DEFAULT '[]', last_used_at TEXT, expires_at TEXT, created_at TEXT DEFAULT (datetime('now')) ); ` const migrationApps = ` CREATE TABLE IF NOT EXISTS apps ( id TEXT PRIMARY KEY, developer_id TEXT NOT NULL REFERENCES developers(id) ON DELETE CASCADE, package_id TEXT UNIQUE NOT NULL, name TEXT NOT NULL, description TEXT, category TEXT, tags TEXT DEFAULT '[]', status TEXT DEFAULT 'draft', created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); ` const migrationAppVersions = ` CREATE TABLE IF NOT EXISTS app_versions ( id TEXT PRIMARY KEY, app_id TEXT NOT NULL REFERENCES apps(id) ON DELETE CASCADE, version_code INTEGER NOT NULL, version_name TEXT NOT NULL, package_url TEXT NOT NULL, package_size INTEGER NOT NULL, signature TEXT NOT NULL, permissions TEXT DEFAULT '[]', min_mosis_version TEXT, release_notes TEXT, status TEXT DEFAULT 'draft', review_notes TEXT, published_at TEXT, created_at TEXT DEFAULT (datetime('now')), UNIQUE(app_id, version_code) ); ` const migrationSigningKeys = ` CREATE TABLE IF NOT EXISTS signing_keys ( id TEXT PRIMARY KEY, developer_id TEXT NOT NULL REFERENCES developers(id) ON DELETE CASCADE, name TEXT NOT NULL, public_key TEXT NOT NULL, fingerprint TEXT NOT NULL, is_active INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now')) ); ` const migrationTelemetry = ` CREATE TABLE IF NOT EXISTS telemetry_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, app_id TEXT NOT NULL, device_id TEXT NOT NULL, event_type TEXT NOT NULL, event_data TEXT, mosis_version TEXT, timestamp TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS crash_reports ( id TEXT PRIMARY KEY, app_id TEXT NOT NULL, app_version TEXT NOT NULL, device_id TEXT NOT NULL, crash_type TEXT NOT NULL, message TEXT, stack_trace TEXT, context TEXT, mosis_version TEXT, timestamp TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS telemetry_daily ( app_id TEXT NOT NULL, date TEXT NOT NULL, event_type TEXT NOT NULL, count INTEGER NOT NULL, unique_devices INTEGER NOT NULL, PRIMARY KEY (app_id, date, event_type) ); ` const migrationAuditLogs = ` CREATE TABLE IF NOT EXISTS audit_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, developer_id TEXT, action TEXT NOT NULL, resource_type TEXT, resource_id TEXT, details TEXT, ip_address TEXT, user_agent TEXT, created_at TEXT DEFAULT (datetime('now')) ); ` const migrationIndexes = ` CREATE INDEX IF NOT EXISTS idx_developers_email ON developers(email); CREATE INDEX IF NOT EXISTS idx_developers_oauth ON developers(oauth_provider, oauth_id); CREATE INDEX IF NOT EXISTS idx_api_keys_developer ON api_keys(developer_id); CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(key_prefix); CREATE INDEX IF NOT EXISTS idx_apps_developer ON apps(developer_id); CREATE INDEX IF NOT EXISTS idx_apps_package ON apps(package_id); CREATE INDEX IF NOT EXISTS idx_apps_status ON apps(status); CREATE INDEX IF NOT EXISTS idx_versions_app ON app_versions(app_id); CREATE INDEX IF NOT EXISTS idx_versions_status ON app_versions(status); CREATE INDEX IF NOT EXISTS idx_signing_keys_developer ON signing_keys(developer_id); CREATE INDEX IF NOT EXISTS idx_signing_keys_fingerprint ON signing_keys(fingerprint); CREATE INDEX IF NOT EXISTS idx_telemetry_app ON telemetry_events(app_id, timestamp); CREATE INDEX IF NOT EXISTS idx_crashes_app ON crash_reports(app_id, timestamp); CREATE INDEX IF NOT EXISTS idx_audit_developer ON audit_logs(developer_id); CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_logs(created_at); `