/** * Database Command * Manage and inspect the database */ import { SlashCommandBuilder, EmbedBuilder, type ChatInputCommandInteraction, codeBlock, } from 'discord.js'; import type { Command } from '../../types/index.ts'; import { PermissionLevel } from '../../types/index.ts'; import type { EllyClient } from '../../client/EllyClient.ts'; export const databaseCommand: Command = { data: new SlashCommandBuilder() .setName('database') .setDescription('Database management commands') .addSubcommand((sub) => sub .setName('stats') .setDescription('View database statistics') ) .addSubcommand((sub) => sub .setName('backup') .setDescription('Create a database backup') ) .addSubcommand((sub) => sub .setName('query') .setDescription('Execute a read-only SQL query') .addStringOption((opt) => opt .setName('sql') .setDescription('The SQL query to execute') .setRequired(true) ) ) .addSubcommand((sub) => sub .setName('tables') .setDescription('List all database tables') ) .addSubcommand((sub) => sub .setName('vacuum') .setDescription('Optimize the database') ), permission: PermissionLevel.Developer, cooldown: 5, async execute(interaction: ChatInputCommandInteraction): Promise { const client = interaction.client as EllyClient; const subcommand = interaction.options.getSubcommand(); // Check if SQLite database is available if (!client.dbManager) { await interaction.reply({ content: '❌ SQLite database is not initialized. Using legacy JSON database.', ephemeral: true, }); return; } switch (subcommand) { case 'stats': await handleStats(interaction, client); break; case 'backup': await handleBackup(interaction, client); break; case 'query': await handleQuery(interaction, client); break; case 'tables': await handleTables(interaction, client); break; case 'vacuum': await handleVacuum(interaction, client); break; } }, }; async function handleStats( interaction: ChatInputCommandInteraction, client: EllyClient ): Promise { await interaction.deferReply({ ephemeral: true }); try { const stats = await client.dbManager!.getStats(); const embed = new EmbedBuilder() .setColor(client.config.colors.primary) .setTitle('📊 Database Statistics') .addFields( { name: '📁 Path', value: `\`${stats.path}\``, inline: false }, { name: '💾 Size', value: formatBytes(stats.size), inline: true }, { name: '🔌 Connected', value: stats.connected ? '✅ Yes' : '❌ No', inline: true }, { name: '📋 Tables', value: String(stats.tables.length), inline: true }, { name: '📝 Table List', value: stats.tables.join(', ') || 'None' } ) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); } catch (error) { await interaction.editReply({ content: `❌ Failed to get database stats: ${error}`, }); } } async function handleBackup( interaction: ChatInputCommandInteraction, client: EllyClient ): Promise { await interaction.deferReply({ ephemeral: true }); try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = `./data/backups/elly_${timestamp}.sqlite`; // Ensure backup directory exists await Deno.mkdir('./data/backups', { recursive: true }); await client.dbManager!.backup(backupPath); const embed = new EmbedBuilder() .setColor(0x57F287) .setTitle('✅ Backup Created') .addFields( { name: '📁 Backup Path', value: `\`${backupPath}\`` }, { name: '⏰ Created At', value: new Date().toISOString() } ) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); } catch (error) { await interaction.editReply({ content: `❌ Failed to create backup: ${error}`, }); } } async function handleQuery( interaction: ChatInputCommandInteraction, client: EllyClient ): Promise { const sql = interaction.options.getString('sql', true); // Security: Only allow SELECT queries const normalizedSql = sql.trim().toLowerCase(); if (!normalizedSql.startsWith('select') && !normalizedSql.startsWith('pragma')) { await interaction.reply({ content: '❌ Only SELECT and PRAGMA queries are allowed.', ephemeral: true, }); return; } // Block dangerous patterns const dangerousPatterns = [ /drop\s+table/i, /delete\s+from/i, /truncate/i, /insert\s+into/i, /update\s+\w+\s+set/i, /alter\s+table/i, /create\s+table/i, ]; for (const pattern of dangerousPatterns) { if (pattern.test(sql)) { await interaction.reply({ content: '❌ Query contains forbidden patterns.', ephemeral: true, }); return; } } await interaction.deferReply({ ephemeral: true }); try { const startTime = performance.now(); const result = client.dbManager!.connection.query(sql); const endTime = performance.now(); if (!result.success) { await interaction.editReply({ content: `❌ Query failed: ${result.error?.message}`, }); return; } const rows = result.data ?? []; let output = JSON.stringify(rows, null, 2); if (output.length > 1800) { output = output.substring(0, 1800) + '\n... (truncated)'; } const embed = new EmbedBuilder() .setColor(client.config.colors.primary) .setTitle('📊 Query Result') .addFields( { name: '📝 Query', value: codeBlock('sql', sql.substring(0, 500)) }, { name: '📤 Result', value: codeBlock('json', output) }, { name: '📊 Rows', value: String(rows.length), inline: true }, { name: '⏱️ Time', value: `${(endTime - startTime).toFixed(2)}ms`, inline: true } ) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); } catch (error) { await interaction.editReply({ content: `❌ Query error: ${error}`, }); } } async function handleTables( interaction: ChatInputCommandInteraction, client: EllyClient ): Promise { await interaction.deferReply({ ephemeral: true }); try { const result = client.dbManager!.connection.query<{ name: string; sql: string }>( "SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name" ); if (!result.success || !result.data) { await interaction.editReply({ content: `❌ Failed to list tables: ${result.error?.message}`, }); return; } const tables = result.data; const tableInfo = tables.map((t) => { // Get row count const countResult = client.dbManager!.connection.queryOne<{ count: number }>( `SELECT COUNT(*) as count FROM ${t.name}` ); const count = countResult.success ? countResult.data?.count ?? 0 : 0; return `**${t.name}** - ${count} rows`; }); const embed = new EmbedBuilder() .setColor(client.config.colors.primary) .setTitle('📋 Database Tables') .setDescription(tableInfo.join('\n') || 'No tables found') .addFields({ name: 'Total Tables', value: String(tables.length), inline: true, }) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); } catch (error) { await interaction.editReply({ content: `❌ Failed to list tables: ${error}`, }); } } async function handleVacuum( interaction: ChatInputCommandInteraction, client: EllyClient ): Promise { await interaction.deferReply({ ephemeral: true }); try { const sizeBefore = await client.dbManager!.connection.getSize(); await client.dbManager!.vacuum(); const sizeAfter = await client.dbManager!.connection.getSize(); const saved = sizeBefore - sizeAfter; const embed = new EmbedBuilder() .setColor(0x57F287) .setTitle('✅ Database Optimized') .addFields( { name: 'Size Before', value: formatBytes(sizeBefore), inline: true }, { name: 'Size After', value: formatBytes(sizeAfter), inline: true }, { name: 'Space Saved', value: formatBytes(saved), inline: true } ) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); } catch (error) { await interaction.editReply({ content: `❌ Failed to vacuum database: ${error}`, }); } } function formatBytes(bytes: number): string { const units = ['B', 'KB', 'MB', 'GB']; let unitIndex = 0; let size = bytes; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)} ${units[unitIndex]}`; }