refactor: Modularize server into separate concerns
Split monolithic index.ts (~1400 lines) into modular structure: - config.ts: Server configuration and constants - db/: Database initialization, migrations, and seeds - routes/: API handlers by domain (themes, canvas, components, etc.) - services/: Terminal WebSocket server - utils/: CORS helpers Entry point now only coordinates initialization.
This commit is contained in:
119
server/routes/database.ts
Normal file
119
server/routes/database.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { db } from '../db'
|
||||
import { jsonResponse, errorResponse } from '../utils/cors'
|
||||
|
||||
export function handleTables() {
|
||||
const tables = db.query(`
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
||||
ORDER BY name
|
||||
`).all() as { name: string }[]
|
||||
|
||||
const result = tables.map(t => {
|
||||
const countResult = db.query(`SELECT COUNT(*) as count FROM "${t.name}"`).get() as { count: number }
|
||||
return { name: t.name, count: countResult.count }
|
||||
})
|
||||
|
||||
return jsonResponse(result)
|
||||
}
|
||||
|
||||
export async function handleStats() {
|
||||
// Get database file size
|
||||
const file = Bun.file('agent-ui.db')
|
||||
const size = file.size
|
||||
const sizeStr = size < 1024 ? `${size} B`
|
||||
: size < 1024 * 1024 ? `${(size / 1024).toFixed(1)} KB`
|
||||
: `${(size / (1024 * 1024)).toFixed(2)} MB`
|
||||
|
||||
// Get tables and counts
|
||||
const tables = db.query(`
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
||||
`).all() as { name: string }[]
|
||||
|
||||
let totalRecords = 0
|
||||
const breakdown = tables.map(t => {
|
||||
const countResult = db.query(`SELECT COUNT(*) as count FROM "${t.name}"`).get() as { count: number }
|
||||
totalRecords += countResult.count
|
||||
return { name: t.name, count: countResult.count }
|
||||
})
|
||||
|
||||
return jsonResponse({
|
||||
size: sizeStr,
|
||||
tables: tables.length,
|
||||
totalRecords,
|
||||
breakdown
|
||||
})
|
||||
}
|
||||
|
||||
export function handleTableSchema(tableName: string) {
|
||||
// Verify table exists
|
||||
const tableExists = db.query(`
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name = ?
|
||||
`).get(tableName)
|
||||
|
||||
if (!tableExists) {
|
||||
return errorResponse('Table not found', 404)
|
||||
}
|
||||
|
||||
const schema = db.query(`PRAGMA table_info("${tableName}")`).all() as any[]
|
||||
const result = schema.map(col => ({
|
||||
name: col.name,
|
||||
type: col.type,
|
||||
notnull: !!col.notnull,
|
||||
pk: !!col.pk
|
||||
}))
|
||||
|
||||
return jsonResponse(result)
|
||||
}
|
||||
|
||||
export function handleTableData(tableName: string, url: URL) {
|
||||
// Verify table exists
|
||||
const tableExists = db.query(`
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name = ?
|
||||
`).get(tableName)
|
||||
|
||||
if (!tableExists) {
|
||||
return errorResponse('Table not found', 404)
|
||||
}
|
||||
|
||||
const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 500)
|
||||
const offset = parseInt(url.searchParams.get('offset') || '0')
|
||||
|
||||
const countResult = db.query(`SELECT COUNT(*) as count FROM "${tableName}"`).get() as { count: number }
|
||||
const rows = db.query(`SELECT * FROM "${tableName}" LIMIT ? OFFSET ?`).all(limit, offset)
|
||||
|
||||
return jsonResponse({
|
||||
total: countResult.count,
|
||||
limit,
|
||||
offset,
|
||||
rows
|
||||
})
|
||||
}
|
||||
|
||||
export async function handleQuery(req: Request) {
|
||||
const body = await req.json()
|
||||
const query = (body.query || '').trim()
|
||||
|
||||
// Security: Only allow SELECT statements
|
||||
const normalizedQuery = query.toLowerCase()
|
||||
if (!normalizedQuery.startsWith('select')) {
|
||||
return errorResponse('Only SELECT queries are allowed for security reasons', 403)
|
||||
}
|
||||
|
||||
// Block dangerous keywords
|
||||
const dangerousKeywords = ['drop', 'delete', 'update', 'insert', 'alter', 'create', 'truncate', 'replace']
|
||||
for (const keyword of dangerousKeywords) {
|
||||
if (normalizedQuery.includes(keyword)) {
|
||||
return errorResponse(`Query contains forbidden keyword: ${keyword.toUpperCase()}`, 403)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const rows = db.query(query).all()
|
||||
return jsonResponse({ rows })
|
||||
} catch (e: any) {
|
||||
return errorResponse(e.message, 400)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user