place_maxlan/server/sqlite-storage.ts
2025-08-21 16:05:29 +02:00

220 lines
7.0 KiB
TypeScript

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<Pixel | undefined> {
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<Pixel[]> {
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<Pixel> {
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<CanvasConfig> {
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<CanvasConfig> {
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<UserCooldown | undefined> {
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<UserCooldown> {
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<Pixel[]> {
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<void> {
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<void> {
this.db.prepare("DELETE FROM pixels WHERE id = ?").run(pixelId);
}
async clearCanvas(): Promise<void> {
this.db.prepare("DELETE FROM pixels").run();
}
}