import { DatabaseSync } from 'node:sqlite'; import path from 'node:path'; import { config } from './config.js'; const db = new DatabaseSync(path.join(config.DATA_DIR, 'playbull.db')); db.exec(` PRAGMA journal_mode = WAL; CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, balance REAL NOT NULL DEFAULT 0, avatar TEXT DEFAULT '🐂', created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS holdings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, symbol TEXT NOT NULL, quantity REAL NOT NULL DEFAULT 0, avg_price REAL NOT NULL DEFAULT 0, UNIQUE(user_id, symbol) ); CREATE TABLE IF NOT EXISTS trades ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, symbol TEXT NOT NULL, side TEXT NOT NULL, qty REAL NOT NULL, price REAL NOT NULL, ts INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS games ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, game TEXT NOT NULL, bet REAL NOT NULL, payout REAL NOT NULL, result TEXT, ts INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS feed ( id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, text TEXT NOT NULL, ts INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS uploads ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, url TEXT NOT NULL, frame TEXT NOT NULL DEFAULT 'gallery', focus_x REAL DEFAULT 0.5, focus_y REAL DEFAULT 0.5, zoom REAL DEFAULT 1.0, created_at INTEGER NOT NULL ); `); // Migration: Avatar-Bild + Positions-Metadaten const userCols = db.prepare('PRAGMA table_info(users)').all().map((c) => c.name); if (!userCols.includes('avatar_image')) db.exec('ALTER TABLE users ADD COLUMN avatar_image TEXT'); if (!userCols.includes('avatar_focus_x')) db.exec('ALTER TABLE users ADD COLUMN avatar_focus_x REAL DEFAULT 0.5'); if (!userCols.includes('avatar_focus_y')) db.exec('ALTER TABLE users ADD COLUMN avatar_focus_y REAL DEFAULT 0.5'); if (!userCols.includes('avatar_zoom')) db.exec('ALTER TABLE users ADD COLUMN avatar_zoom REAL DEFAULT 1.0'); export default db; // ---- User Helpers ---- export const Users = { create(username, hash, balance) { const info = db.prepare( 'INSERT INTO users (username, password_hash, balance, created_at) VALUES (?,?,?,?)' ).run(username, hash, balance, Date.now()); return this.byId(info.lastInsertRowid); }, byId(id) { return db.prepare('SELECT * FROM users WHERE id = ?').get(id); }, byName(name) { return db.prepare('SELECT * FROM users WHERE username = ? COLLATE NOCASE').get(name); }, all() { return db.prepare(` SELECT id, username, balance, avatar, avatar_image, avatar_focus_x, avatar_focus_y, avatar_zoom, created_at FROM users ORDER BY balance DESC `).all(); }, setBalance(id, balance) { db.prepare('UPDATE users SET balance = ? WHERE id = ?').run(Math.max(0, balance), id); }, addBalance(id, delta) { const u = this.byId(id); if (!u) return null; const nb = Math.max(0, u.balance + delta); db.prepare('UPDATE users SET balance = ? WHERE id = ?').run(nb, id); return nb; }, setAvatar(id, avatar) { db.prepare('UPDATE users SET avatar = ? WHERE id = ?').run(avatar, id); }, setAvatarImage(id, imagePath, focusX, focusY, zoom) { db.prepare(` UPDATE users SET avatar_image = ?, avatar_focus_x = ?, avatar_focus_y = ?, avatar_zoom = ? WHERE id = ? `).run(imagePath, focusX, focusY, zoom, id); }, setAvatarPosition(id, focusX, focusY, zoom) { db.prepare(` UPDATE users SET avatar_focus_x = ?, avatar_focus_y = ?, avatar_zoom = ? WHERE id = ? `).run(focusX, focusY, zoom, id); }, clearAvatarImage(id) { db.prepare(` UPDATE users SET avatar_image = NULL, avatar_focus_x = 0.5, avatar_focus_y = 0.5, avatar_zoom = 1.0 WHERE id = ? `).run(id); }, count() { return db.prepare('SELECT COUNT(*) c FROM users').get().c; }, }; // ---- Holdings Helpers ---- export const Holdings = { forUser(userId) { return db.prepare('SELECT * FROM holdings WHERE user_id = ?').all(userId); }, get(userId, symbol) { return db.prepare('SELECT * FROM holdings WHERE user_id = ? AND symbol = ?').get(userId, symbol); }, upsert(userId, symbol, quantity, avgPrice) { db.prepare(` INSERT INTO holdings (user_id, symbol, quantity, avg_price) VALUES (?,?,?,?) ON CONFLICT(user_id, symbol) DO UPDATE SET quantity = excluded.quantity, avg_price = excluded.avg_price `).run(userId, symbol, quantity, avgPrice); }, remove(userId, symbol) { db.prepare('DELETE FROM holdings WHERE user_id = ? AND symbol = ?').run(userId, symbol); }, }; export const Trades = { add(userId, symbol, side, qty, price) { db.prepare('INSERT INTO trades (user_id, symbol, side, qty, price, ts) VALUES (?,?,?,?,?,?)') .run(userId, symbol, side, qty, price, Date.now()); }, forUser(userId, limit = 50) { return db.prepare('SELECT * FROM trades WHERE user_id = ? ORDER BY ts DESC LIMIT ?').all(userId, limit); }, }; export const Games = { add(userId, game, bet, payout, result) { db.prepare('INSERT INTO games (user_id, game, bet, payout, result, ts) VALUES (?,?,?,?,?,?)') .run(userId, game, bet, payout, JSON.stringify(result || {}), Date.now()); }, forUser(userId, limit = 50) { return db.prepare('SELECT * FROM games WHERE user_id = ? ORDER BY ts DESC LIMIT ?').all(userId, limit); }, }; export const Feed = { add(kind, text) { db.prepare('INSERT INTO feed (kind, text, ts) VALUES (?,?,?)').run(kind, text, Date.now()); // alte Eintraege aufraeumen db.prepare('DELETE FROM feed WHERE id NOT IN (SELECT id FROM feed ORDER BY ts DESC LIMIT 100)').run(); }, recent(limit = 30) { return db.prepare('SELECT * FROM feed ORDER BY ts DESC LIMIT ?').all(limit); }, }; export const Uploads = { forUser(userId) { return db.prepare('SELECT * FROM uploads WHERE user_id = ? ORDER BY created_at DESC').all(userId); }, byId(id) { return db.prepare('SELECT * FROM uploads WHERE id = ?').get(id); }, create(userId, frame, focusX, focusY, zoom) { const info = db.prepare(` INSERT INTO uploads (user_id, url, frame, focus_x, focus_y, zoom, created_at) VALUES (?,?,?,?,?,?,?) `).run(userId, 'pending', frame, focusX, focusY, zoom, Date.now()); return this.byId(info.lastInsertRowid); }, setUrl(id, url) { db.prepare('UPDATE uploads SET url = ? WHERE id = ?').run(url, id); return this.byId(id); }, setPosition(id, focusX, focusY, zoom) { db.prepare('UPDATE uploads SET focus_x = ?, focus_y = ?, zoom = ? WHERE id = ?') .run(focusX, focusY, zoom, id); return this.byId(id); }, remove(id) { db.prepare('DELETE FROM uploads WHERE id = ?').run(id); }, };