import Database from 'better-sqlite3'; import { type Pixel, type InsertPixel, type CanvasConfig, type InsertCanvasConfig, type UserCooldown, type InsertUserCooldown } from "@shared/schema"; import { randomUUID } from "crypto"; import { config } from "./config"; import { IStorage } from "./storage"; export class SQLiteStorage implements IStorage { private db: Database.Database; constructor(dbPath: string = ':memory:') { this.db = new Database(dbPath); this.initTables(); this.initDefaultConfig(); } private initTables() { // Pixels table this.db.exec(` CREATE TABLE IF NOT EXISTS pixels ( id TEXT PRIMARY KEY DEFAULT (hex(randomblob(16))), x INTEGER NOT NULL, y INTEGER NOT NULL, color TEXT NOT NULL, userId TEXT NOT NULL, username TEXT NOT NULL, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Canvas config table this.db.exec(` CREATE TABLE IF NOT EXISTS canvas_config ( id TEXT PRIMARY KEY DEFAULT (hex(randomblob(16))), canvasWidth INTEGER NOT NULL DEFAULT 100, canvasHeight INTEGER NOT NULL DEFAULT 100, defaultCooldown INTEGER NOT NULL DEFAULT 5, enableAutomaticEvents BOOLEAN NOT NULL DEFAULT 0, eventDuration INTEGER NOT NULL DEFAULT 30, eventInterval INTEGER NOT NULL DEFAULT 6, updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // User cooldowns table this.db.exec(` CREATE TABLE IF NOT EXISTS user_cooldowns ( id TEXT PRIMARY KEY DEFAULT (hex(randomblob(16))), userId TEXT NOT NULL UNIQUE, lastPlacement DATETIME DEFAULT CURRENT_TIMESTAMP, cooldownEnds DATETIME NOT NULL ) `); // Create indexes this.db.exec(`CREATE INDEX IF NOT EXISTS idx_pixels_xy ON pixels(x, y)`); this.db.exec(`CREATE INDEX IF NOT EXISTS idx_pixels_created ON pixels(createdAt DESC)`); this.db.exec(`CREATE INDEX IF NOT EXISTS idx_cooldowns_user ON user_cooldowns(userId)`); } private initDefaultConfig() { const existingConfig = this.db.prepare('SELECT * FROM canvas_config LIMIT 1').get(); if (!existingConfig) { this.db.prepare(` INSERT INTO canvas_config (canvasWidth, canvasHeight, defaultCooldown, enableAutomaticEvents, eventDuration, eventInterval) VALUES (?, ?, ?, ?, ?, ?) `).run( config.canvasWidth, config.canvasHeight, config.defaultCooldown, config.enableAutomaticEvents ? 1 : 0, config.eventDurationMinutes, config.eventIntervalHours ); } } async getPixel(x: number, y: number): Promise { const row = this.db.prepare('SELECT * FROM pixels WHERE x = ? AND y = ? ORDER BY createdAt DESC LIMIT 1').get(x, y) as any; if (!row) return undefined; return { ...row, createdAt: new Date(row.createdAt), enableAutomaticEvents: Boolean(row.enableAutomaticEvents) }; } async getAllPixels(): Promise { const rows = this.db.prepare(` SELECT p1.* FROM pixels p1 INNER JOIN ( SELECT x, y, MAX(createdAt) as maxCreated FROM pixels GROUP BY x, y ) p2 ON p1.x = p2.x AND p1.y = p2.y AND p1.createdAt = p2.maxCreated `).all() as any[]; return rows.map(row => ({ ...row, createdAt: new Date(row.createdAt) })); } async placePixel(insertPixel: InsertPixel): Promise { const id = randomUUID(); const now = new Date(); this.db.prepare(` INSERT INTO pixels (id, x, y, color, userId, username, createdAt) VALUES (?, ?, ?, ?, ?, ?, ?) `).run(id, insertPixel.x, insertPixel.y, insertPixel.color, insertPixel.userId, insertPixel.username, now.toISOString()); return { id, ...insertPixel, createdAt: now }; } async getCanvasConfig(): Promise { const row = this.db.prepare('SELECT * FROM canvas_config ORDER BY updatedAt DESC LIMIT 1').get() as any; return { ...row, enableAutomaticEvents: Boolean(row.enableAutomaticEvents), updatedAt: new Date(row.updatedAt) }; } async updateCanvasConfig(configUpdate: InsertCanvasConfig): Promise { const currentConfig = await this.getCanvasConfig(); const now = new Date(); this.db.prepare(` UPDATE canvas_config SET canvasWidth = ?, canvasHeight = ?, defaultCooldown = ?, enableAutomaticEvents = ?, eventDuration = ?, eventInterval = ?, updatedAt = ? WHERE id = ? `).run( configUpdate.canvasWidth ?? currentConfig.canvasWidth, configUpdate.canvasHeight ?? currentConfig.canvasHeight, configUpdate.defaultCooldown ?? currentConfig.defaultCooldown, configUpdate.enableAutomaticEvents ? 1 : 0, configUpdate.eventDuration ?? currentConfig.eventDuration, configUpdate.eventInterval ?? currentConfig.eventInterval, now.toISOString(), currentConfig.id ); return this.getCanvasConfig(); } async getUserCooldown(userId: string): Promise { const row = this.db.prepare('SELECT * FROM user_cooldowns WHERE userId = ?').get(userId) as any; if (!row) return undefined; return { ...row, lastPlacement: new Date(row.lastPlacement), cooldownEnds: new Date(row.cooldownEnds) }; } async setUserCooldown(insertCooldown: InsertUserCooldown): Promise { const id = randomUUID(); const now = new Date(); this.db.prepare(` INSERT OR REPLACE INTO user_cooldowns (id, userId, lastPlacement, cooldownEnds) VALUES (?, ?, ?, ?) `).run(id, insertCooldown.userId, now.toISOString(), insertCooldown.cooldownEnds.toISOString()); return { id, userId: insertCooldown.userId, lastPlacement: now, cooldownEnds: insertCooldown.cooldownEnds }; } async getRecentPlacements(limit: number = 10): Promise { const rows = this.db.prepare(` SELECT * FROM pixels ORDER BY createdAt DESC LIMIT ? `).all(limit) as any[]; return rows.map(row => ({ ...row, createdAt: new Date(row.createdAt) })); } // Canvas-Erweiterungsmethode async expandCanvas(newWidth: number, newHeight: number): Promise { const currentConfig = await this.getCanvasConfig(); if (newWidth < currentConfig.canvasWidth || newHeight < currentConfig.canvasHeight) { throw new Error("Canvas kann nur erweitert werden, nicht verkleinert"); } await this.updateCanvasConfig({ canvasWidth: newWidth, canvasHeight: newHeight, defaultCooldown: currentConfig.defaultCooldown, enableAutomaticEvents: currentConfig.enableAutomaticEvents, eventDuration: currentConfig.eventDuration, eventInterval: currentConfig.eventInterval }); } async deletePixel(pixelId: string): Promise { this.db.prepare("DELETE FROM pixels WHERE id = ?").run(pixelId); } async clearCanvas(): Promise { this.db.prepare("DELETE FROM pixels").run(); } }