const Database = require('better-sqlite3'); import path from 'path'; import fs from 'fs'; const dbPath = path.join(__dirname, '../data/invicanvas.db'); const dbDir = path.dirname(dbPath); if (!fs.existsSync(dbDir)) { fs.mkdirSync(dbDir, { recursive: true }); } export const db = new Database(dbPath); db.pragma('journal_mode = WAL'); db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, github_id TEXT UNIQUE, username TEXT NOT NULL, display_name TEXT, avatar_url TEXT, email TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS pixels ( x INTEGER NOT NULL, y INTEGER NOT NULL, color TEXT NOT NULL, user_id INTEGER, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (x, y), FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE INDEX IF NOT EXISTS idx_pixels_updated ON pixels(updated_at); `); const tableInfo = db.pragma('table_info(pixels)') as { name: string }[]; const hasUserId = tableInfo.some(col => col.name === 'user_id'); if (!hasUserId) { db.exec('ALTER TABLE pixels ADD COLUMN user_id INTEGER REFERENCES users(id)'); } const indexes = db.pragma('index_list(pixels)') as { name: string }[]; const hasIndex = indexes.some(idx => idx.name === 'idx_pixels_updated'); if (!hasIndex) { db.exec('CREATE INDEX idx_pixels_updated ON pixels(updated_at)'); } export function generateUserId(): string { return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } function isValidColor(color: string): boolean { return /^#[0-9A-Fa-f]{6}$/.test(color); } export function validatePixel(pixel: { x: number; y: number; color: string }): string | null { if (!Number.isInteger(pixel.x) || pixel.x < -1000000 || pixel.x > 1000000) { return 'Invalid x coordinate'; } if (!Number.isInteger(pixel.y) || pixel.y < -1000000 || pixel.y > 1000000) { return 'Invalid y coordinate'; } if (!isValidColor(pixel.color)) { return 'Invalid color format'; } return null; } export function upsertPixelsWithConflictResolution( pixels: { x: number; y: number; color: string; userId?: number }[], clientTimestamp: number ): { saved: number; conflicts: number } { const getStmt = db.prepare('SELECT updated_at FROM pixels WHERE x = ? AND y = ?'); const stmt = db.prepare( 'INSERT OR REPLACE INTO pixels (x, y, color, user_id, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)' ); let saved = 0; let conflicts = 0; const transaction = db.transaction((pixels: any[]) => { for (const pixel of pixels) { try { const existing = getStmt.get(pixel.x, pixel.y) as { updated_at: string } | undefined; if (existing) { const existingTime = new Date(existing.updated_at).getTime(); if (clientTimestamp && existingTime > clientTimestamp) { conflicts++; continue; } } stmt.run(pixel.x, pixel.y, pixel.color, pixel.userId || null); saved++; } catch (err) { console.error('Error upserting pixel:', err); } } }); transaction(pixels); return { saved, conflicts }; } export function getPixelsInRange( minX: number, maxX: number, minY: number, maxY: number ): { x: number; y: number; color: string }[] { const stmt = db.prepare( 'SELECT x, y, color FROM pixels WHERE x >= ? AND x <= ? AND y >= ? AND y <= ? ORDER BY x, y' ); return stmt.all(minX, maxX, minY, maxY) as { x: number; y: number; color: string }[]; } export function getAllPixels(): { x: number; y: number; color: string }[] { const stmt = db.prepare('SELECT x, y, color FROM pixels ORDER BY x, y'); return stmt.all() as { x: number; y: number; color: string }[]; }