feat: implement database management API with MySQL driver and schema operations

This commit is contained in:
Ümit Tunç
2026-04-28 20:19:56 +03:00
parent 2e529bb61c
commit 01ddb81aa9
7 changed files with 276 additions and 9 deletions
@@ -83,4 +83,14 @@ interface DatabaseDriverInterface
* Create a new database.
*/
public function createDatabase(string $name, string $charset = 'utf8mb4', string $collation = 'utf8mb4_unicode_ci'): bool;
/**
* Drop a database.
*/
public function dropDatabase(string $name): bool;
/**
* Rename a database.
*/
public function renameDatabase(string $oldName, string $newName): bool;
}
@@ -287,4 +287,31 @@ class SchemaController extends Controller
return Response::json(['error' => $e->getMessage()], 400);
}
}
public function dropDatabase(Request $request, $database)
{
try {
$this->initializeDriver($request);
$this->databaseService->dropDatabase($database);
return Response::json(['message' => "Database '{$database}' dropped successfully"]);
} catch (\Exception $e) {
return Response::json(['error' => $e->getMessage()], 400);
}
}
public function renameDatabase(Request $request, $database)
{
$request->validate([
'newName' => 'required|string|max:64',
]);
try {
$this->initializeDriver($request);
$newName = $request->get('newName');
$this->databaseService->renameDatabase($database, $newName);
return Response::json(['message' => "Database '{$database}' renamed to '{$newName}' successfully"]);
} catch (\Exception $e) {
return Response::json(['error' => $e->getMessage()], 400);
}
}
}
@@ -454,4 +454,39 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface
DB::connection($this->connectionName)->statement("CREATE DATABASE `{$name}` CHARACTER SET {$charset} COLLATE {$collation}");
return true;
}
public function dropDatabase(string $name): bool
{
DB::connection($this->connectionName)->statement("DROP DATABASE `{$name}`");
return true;
}
public function renameDatabase(string $oldName, string $newName): bool
{
// 1. Create new database
$this->createDatabase($newName);
// 2. Get tables from old database
$tables = $this->query("SHOW TABLES FROM `{$oldName}`");
$key = "Tables_in_{$oldName}";
if (!empty($tables)) {
$renameQueries = [];
foreach ($tables as $table) {
$tableName = $table->$key;
$renameQueries[] = "`{$oldName}`.`{$tableName}` TO `{$newName}`.`{$tableName}`";
}
// 3. Rename all tables
if (!empty($renameQueries)) {
$sql = "RENAME TABLE " . implode(', ', $renameQueries);
DB::connection($this->connectionName)->statement($sql);
}
}
// 4. Drop old database
$this->dropDatabase($oldName);
return true;
}
}
+16
View File
@@ -172,4 +172,20 @@ class DatabaseService
{
return $this->getDriver()->createDatabase($name, $charset, $collation);
}
/**
* Drop a database.
*/
public function dropDatabase(string $name): bool
{
return $this->getDriver()->dropDatabase($name);
}
/**
* Rename a database.
*/
public function renameDatabase(string $oldName, string $newName): bool
{
return $this->getDriver()->renameDatabase($oldName, $newName);
}
}
+2
View File
@@ -11,6 +11,8 @@ Route::get('/user', function (Request $request) {
Route::prefix('schema')->group(function () {
Route::get('/databases', [SchemaController::class, 'databases']);
Route::post('/databases', [SchemaController::class, 'createDatabase']);
Route::delete('/databases/{database}', [SchemaController::class, 'dropDatabase']);
Route::post('/databases/{database}/rename', [SchemaController::class, 'renameDatabase']);
Route::get('/tables/{database}', [SchemaController::class, 'tables']);
Route::get('/metadata/{database}', [SchemaController::class, 'metadata']);
Route::get('/metadata/{database}/tables', [SchemaController::class, 'tablesMetadata']);
+184 -9
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState, useMemo } from 'react';
import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography, Divider, CircularProgress, Stack, TextField, InputAdornment, IconButton, Tooltip, Dialog, DialogTitle, DialogContent, DialogActions, Button, Alert, Snackbar, AlertTitle } from '@mui/material';
import { Storage, Search, FilterList, ChevronRight, ExpandMore, TableChart, Folder, Add } from '@mui/icons-material';
import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography, Divider, CircularProgress, Stack, TextField, InputAdornment, IconButton, Tooltip, Dialog, DialogTitle, DialogContent, DialogActions, Button, Alert, Snackbar, AlertTitle, Menu, MenuItem } from '@mui/material';
import { Storage, Search, FilterList, ChevronRight, ExpandMore, TableChart, Folder, Add, MoreVert, Delete, Edit } from '@mui/icons-material';
import { useAppStore } from '../store/useAppStore';
import { SchemaService } from '../services/api';
import type { MainNotification } from '../types/database';
@@ -19,7 +19,19 @@ const Sidebar: React.FC = () => {
const [openNewDbDialog, setOpenNewDbDialog] = useState(false);
const [newDbName, setNewDbName] = useState('');
const [isCreatingDb, setIsCreatingDb] = useState(false);
const [openRenameDialog, setOpenRenameDialog] = useState(false);
const [dbToRename, setDbToRename] = useState('');
const [renamedDbName, setRenamedDbName] = useState('');
const [openDeleteConfirm, setOpenDeleteConfirm] = useState(false);
const [dbToDelete, setDbToDelete] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
const [selectedDbForMenu, setSelectedDbForMenu] = useState('');
const [notification, setNotification] = useState<MainNotification>({
open: false,
message: '',
@@ -102,7 +114,7 @@ const Sidebar: React.FC = () => {
const handleCreateDatabase = async () => {
if (!newDbName.trim()) return;
setIsCreatingDb(true);
setIsProcessing(true);
try {
await SchemaService.createDatabase(newDbName);
setNotification({
@@ -122,10 +134,95 @@ const Sidebar: React.FC = () => {
severity: 'error'
});
} finally {
setIsCreatingDb(false);
setIsProcessing(false);
}
};
const handleRenameDatabase = async () => {
if (!renamedDbName.trim() || !dbToRename) return;
setIsProcessing(true);
try {
await SchemaService.renameDatabase(dbToRename, renamedDbName);
setNotification({
open: true,
title: 'Success',
message: `Database '${dbToRename}' renamed to '${renamedDbName}' successfully.`,
severity: 'success'
});
if (activeDatabase === dbToRename) {
setActiveDatabase(renamedDbName);
}
setOpenRenameDialog(false);
setDbToRename('');
setRenamedDbName('');
fetchDatabases();
} catch (error: any) {
setNotification({
open: true,
title: 'Error',
message: error.response?.data?.error || error.message,
severity: 'error'
});
} finally {
setIsProcessing(false);
}
};
const handleDeleteDatabase = async () => {
if (!dbToDelete) return;
setIsProcessing(true);
try {
await SchemaService.dropDatabase(dbToDelete);
setNotification({
open: true,
title: 'Success',
message: `Database '${dbToDelete}' deleted successfully.`,
severity: 'success'
});
if (activeDatabase === dbToDelete) {
setActiveDatabase(null);
}
setOpenDeleteConfirm(false);
setDbToDelete('');
fetchDatabases();
} catch (error: any) {
setNotification({
open: true,
title: 'Error',
message: error.response?.data?.error || error.message,
severity: 'error'
});
} finally {
setIsProcessing(false);
}
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, db: string) => {
event.stopPropagation();
setMenuAnchorEl(event.currentTarget);
setSelectedDbForMenu(db);
};
const handleMenuClose = () => {
setMenuAnchorEl(null);
setSelectedDbForMenu('');
};
const handleMenuRename = () => {
setDbToRename(selectedDbForMenu);
setRenamedDbName(selectedDbForMenu);
setOpenRenameDialog(true);
handleMenuClose();
};
const handleMenuDelete = () => {
setDbToDelete(selectedDbForMenu);
setOpenDeleteConfirm(true);
handleMenuClose();
};
return (
<Box sx={{ width: 280, borderRight: 1, borderColor: 'divider', display: 'flex', flexDirection: 'column', bgcolor: 'background.paper', zIndex: 10 }}>
<Box sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
@@ -186,6 +283,9 @@ const Sidebar: React.FC = () => {
<ListItemText
primary={<Typography variant="body2" sx={{ fontWeight: activeDatabase === db ? 600 : 400, fontSize: '0.85rem' }}>{db}</Typography>}
/>
<IconButton size="small" onClick={(e) => handleMenuOpen(e, db)} sx={{ opacity: 0, '&:hover': { opacity: 1 }, '.MuiListItemButton-root:hover &': { opacity: 0.5 } }}>
<MoreVert fontSize="inherit" />
</IconButton>
</ListItemButton>
</ListItem>
@@ -290,24 +390,99 @@ const Sidebar: React.FC = () => {
variant="outlined"
value={newDbName}
onChange={(e) => setNewDbName(e.target.value)}
disabled={isCreatingDb}
disabled={isProcessing}
onKeyDown={(e) => {
if (e.key === 'Enter') handleCreateDatabase();
}}
/>
</DialogContent>
<DialogActions sx={{ p: 2, pt: 0 }}>
<Button onClick={() => setOpenNewDbDialog(false)} disabled={isCreatingDb}>Cancel</Button>
<Button onClick={() => setOpenNewDbDialog(false)} disabled={isProcessing}>Cancel</Button>
<Button
onClick={handleCreateDatabase}
variant="contained"
disabled={isCreatingDb || !newDbName.trim()}
disabled={isProcessing || !newDbName.trim()}
>
{isCreatingDb ? <CircularProgress size={24} /> : 'Create'}
{isProcessing ? <CircularProgress size={24} /> : 'Create'}
</Button>
</DialogActions>
</Dialog>
{/* Rename Database Dialog */}
<Dialog open={openRenameDialog} onClose={() => setOpenRenameDialog(false)} maxWidth="xs" fullWidth>
<DialogTitle sx={{ fontWeight: 800 }}>Rename Database</DialogTitle>
<DialogContent>
<Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
Renaming database <strong>{dbToRename}</strong>.
This will create a new database and move all tables.
</Typography>
<TextField
autoFocus
margin="dense"
label="New Database Name"
type="text"
fullWidth
variant="outlined"
value={renamedDbName}
onChange={(e) => setRenamedDbName(e.target.value)}
disabled={isProcessing}
onKeyDown={(e) => {
if (e.key === 'Enter') handleRenameDatabase();
}}
/>
</DialogContent>
<DialogActions sx={{ p: 2, pt: 0 }}>
<Button onClick={() => setOpenRenameDialog(false)} disabled={isProcessing}>Cancel</Button>
<Button
onClick={handleRenameDatabase}
variant="contained"
disabled={isProcessing || !renamedDbName.trim() || renamedDbName === dbToRename}
>
{isProcessing ? <CircularProgress size={24} /> : 'Rename'}
</Button>
</DialogActions>
</Dialog>
{/* Delete Database Confirm */}
<Dialog open={openDeleteConfirm} onClose={() => setOpenDeleteConfirm(false)}>
<DialogTitle sx={{ fontWeight: 800, color: 'error.main' }}>Delete Database?</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete database <strong>{dbToDelete}</strong>?
This action cannot be undone and all data will be lost.
</Typography>
</DialogContent>
<DialogActions sx={{ p: 2, pt: 0 }}>
<Button onClick={() => setOpenDeleteConfirm(false)} disabled={isProcessing}>Cancel</Button>
<Button
onClick={handleDeleteDatabase}
variant="contained"
color="error"
disabled={isProcessing}
>
{isProcessing ? <CircularProgress size={24} color="inherit" /> : 'Delete'}
</Button>
</DialogActions>
</Dialog>
{/* Database Item Menu */}
<Menu
anchorEl={menuAnchorEl}
open={Boolean(menuAnchorEl)}
onClose={handleMenuClose}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
<MenuItem onClick={handleMenuRename}>
<ListItemIcon><Edit fontSize="small" /></ListItemIcon>
<ListItemText>Rename</ListItemText>
</MenuItem>
<MenuItem onClick={handleMenuDelete} sx={{ color: 'error.main' }}>
<ListItemIcon><Delete fontSize="small" sx={{ color: 'error.main' }} /></ListItemIcon>
<ListItemText>Delete</ListItemText>
</MenuItem>
</Menu>
{/* Local Notification */}
<Snackbar
open={notification.open}
+2
View File
@@ -25,6 +25,8 @@ export default api;
export const SchemaService = {
getDatabases: () => api.get('/schema/databases'),
createDatabase: (name: string, charset?: string, collation?: string) => api.post('/schema/databases', { name, charset, collation }),
dropDatabase: (db: string) => api.delete(`/schema/databases/${db}`, { params: { database: db } }),
renameDatabase: (db: string, newName: string) => api.post(`/schema/databases/${db}/rename`, { newName }, { params: { database: db } }),
getTables: (db: string) => api.get(`/schema/tables/${db}`, { params: { database: db } }),
getDatabaseMetadata: (db: string) => api.get(`/schema/metadata/${db}`, { params: { database: db } }),
getTablesMetadata: (db: string) => api.get(`/schema/metadata/${db}/tables`, { params: { database: db } }),