Phase 3: Express backend, SQLite persistence, auto-save
- server/db.ts: sql.js with migration system (hex_maps, hexes, hex_features) - server/routes/maps.ts: CRUD for hex maps - server/routes/hexes.ts: Bulk hex upsert, region load, sparse storage - server/index.ts: Express 5, CORS, tile serving, SPA fallback - src/data/api-client.ts: Frontend HTTP client for all API endpoints - src/main.ts: Auto-save with 1s debounce, load map state on startup - Port 3002 (Kiepenkerl uses 3001) - Graceful fallback when API unavailable (works without server too) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
85
server/routes/maps.ts
Normal file
85
server/routes/maps.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Router } from 'express';
|
||||
import { getDb, saveDb } from '../db.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// List all maps
|
||||
router.get('/', (_req, res) => {
|
||||
const db = getDb();
|
||||
const rows = db.exec('SELECT * FROM hex_maps ORDER BY updated_at DESC');
|
||||
if (rows.length === 0) {
|
||||
res.json([]);
|
||||
return;
|
||||
}
|
||||
const maps = rows[0].values.map(row => rowToMap(rows[0].columns, row));
|
||||
res.json(maps);
|
||||
});
|
||||
|
||||
// Get single map
|
||||
router.get('/:id', (req, res) => {
|
||||
const db = getDb();
|
||||
const rows = db.exec('SELECT * FROM hex_maps WHERE id = ?', [req.params.id]);
|
||||
if (rows.length === 0 || rows[0].values.length === 0) {
|
||||
res.status(404).json({ error: 'Map not found' });
|
||||
return;
|
||||
}
|
||||
res.json(rowToMap(rows[0].columns, rows[0].values[0]));
|
||||
});
|
||||
|
||||
// Create map
|
||||
router.post('/', (req, res) => {
|
||||
const db = getDb();
|
||||
const { name, image_width, image_height, tile_url, min_zoom, max_zoom, hex_size, origin_x, origin_y } = req.body;
|
||||
db.run(
|
||||
`INSERT INTO hex_maps (name, image_width, image_height, tile_url, min_zoom, max_zoom, hex_size, origin_x, origin_y)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[name ?? 'Untitled', image_width ?? 8000, image_height ?? 12000,
|
||||
tile_url ?? '/tiles/{z}/{x}/{y}.jpg', min_zoom ?? 0, max_zoom ?? 6,
|
||||
hex_size ?? 48, origin_x ?? 0, origin_y ?? 0],
|
||||
);
|
||||
const idRows = db.exec('SELECT last_insert_rowid()');
|
||||
const id = idRows[0].values[0][0];
|
||||
saveDb();
|
||||
res.status(201).json({ id });
|
||||
});
|
||||
|
||||
// Update map
|
||||
router.put('/:id', (req, res) => {
|
||||
const db = getDb();
|
||||
const { name, hex_size, origin_x, origin_y } = req.body;
|
||||
const sets: string[] = [];
|
||||
const params: any[] = [];
|
||||
|
||||
if (name !== undefined) { sets.push('name = ?'); params.push(name); }
|
||||
if (hex_size !== undefined) { sets.push('hex_size = ?'); params.push(hex_size); }
|
||||
if (origin_x !== undefined) { sets.push('origin_x = ?'); params.push(origin_x); }
|
||||
if (origin_y !== undefined) { sets.push('origin_y = ?'); params.push(origin_y); }
|
||||
|
||||
if (sets.length === 0) {
|
||||
res.status(400).json({ error: 'No fields to update' });
|
||||
return;
|
||||
}
|
||||
|
||||
sets.push("updated_at = datetime('now')");
|
||||
params.push(req.params.id);
|
||||
|
||||
db.run(`UPDATE hex_maps SET ${sets.join(', ')} WHERE id = ?`, params);
|
||||
saveDb();
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Delete map
|
||||
router.delete('/:id', (req, res) => {
|
||||
const db = getDb();
|
||||
db.run('DELETE FROM hex_maps WHERE id = ?', [req.params.id]);
|
||||
saveDb();
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
function rowToMap(columns: string[], values: any[]) {
|
||||
const obj: any = {};
|
||||
columns.forEach((col, i) => { obj[col] = values[i]; });
|
||||
return obj;
|
||||
}
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user