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.
120 lines
3.4 KiB
TypeScript
120 lines
3.4 KiB
TypeScript
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)
|
|
}
|
|
}
|