220 lines
7.0 KiB
TypeScript
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();
|
|
}
|
|
} |