feat: implement database management service layer with MySQL driver and API controllers

This commit is contained in:
Ümit Tunç
2026-04-28 20:16:28 +03:00
parent b5282df56f
commit 2e529bb61c
7 changed files with 153 additions and 15 deletions
@@ -78,4 +78,9 @@ interface DatabaseDriverInterface
* Perform batch update on a table.
*/
public function batchUpdate(string $table, array $changes): bool;
/**
* Create a new database.
*/
public function createDatabase(string $name, string $charset = 'utf8mb4', string $collation = 'utf8mb4_unicode_ci'): bool;
}
@@ -266,4 +266,25 @@ class SchemaController extends Controller
return Response::json(['error' => $e->getMessage()], 400);
}
}
public function createDatabase(Request $request)
{
$request->validate([
'name' => 'required|string|max:64',
'charset' => 'nullable|string|max:32',
'collation' => 'nullable|string|max:64',
]);
try {
$this->initializeDriver($request);
$name = $request->get('name');
$charset = $request->get('charset', 'utf8mb4');
$collation = $request->get('collation', 'utf8mb4_unicode_ci');
$this->databaseService->createDatabase($name, $charset, $collation);
return Response::json(['message' => "Database '{$name}' created successfully"]);
} catch (\Exception $e) {
return Response::json(['error' => $e->getMessage()], 400);
}
}
}
@@ -448,4 +448,10 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface
fclose($handle);
return $path;
}
public function createDatabase(string $name, string $charset = 'utf8mb4', string $collation = 'utf8mb4_unicode_ci'): bool
{
DB::connection($this->connectionName)->statement("CREATE DATABASE `{$name}` CHARACTER SET {$charset} COLLATE {$collation}");
return true;
}
}
+8
View File
@@ -164,4 +164,12 @@ class DatabaseService
{
return $this->getDriver()->batchUpdate($table, $changes);
}
/**
* Create a new database.
*/
public function createDatabase(string $name, string $charset = 'utf8mb4', string $collation = 'utf8mb4_unicode_ci'): bool
{
return $this->getDriver()->createDatabase($name, $charset, $collation);
}
}
+1
View File
@@ -10,6 +10,7 @@ Route::get('/user', function (Request $request) {
Route::prefix('schema')->group(function () {
Route::get('/databases', [SchemaController::class, 'databases']);
Route::post('/databases', [SchemaController::class, 'createDatabase']);
Route::get('/tables/{database}', [SchemaController::class, 'tables']);
Route::get('/metadata/{database}', [SchemaController::class, 'metadata']);
Route::get('/metadata/{database}/tables', [SchemaController::class, 'tablesMetadata']);
+101 -5
View File
@@ -1,8 +1,9 @@
import React, { useEffect, useState, useMemo } from 'react';
import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography, Divider, CircularProgress, Stack, TextField, InputAdornment } from '@mui/material';
import { Storage, Search, FilterList, ChevronRight, ExpandMore, TableChart, Folder } 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 } from '@mui/material';
import { Storage, Search, FilterList, ChevronRight, ExpandMore, TableChart, Folder, Add } from '@mui/icons-material';
import { useAppStore } from '../store/useAppStore';
import { SchemaService } from '../services/api';
import type { MainNotification } from '../types/database';
const Sidebar: React.FC = () => {
const { activeDatabase, setActiveDatabase, setActiveTable, activeTable } = useAppStore();
@@ -16,9 +17,18 @@ const Sidebar: React.FC = () => {
const [tablesLoading, setTablesLoading] = useState(false);
// Fetch databases on mount
useEffect(() => {
const [openNewDbDialog, setOpenNewDbDialog] = useState(false);
const [newDbName, setNewDbName] = useState('');
const [isCreatingDb, setIsCreatingDb] = useState(false);
const [notification, setNotification] = useState<MainNotification>({
open: false,
message: '',
title: '',
severity: 'success'
});
const fetchDatabases = async () => {
setLoading(true);
try {
const response = await SchemaService.getDatabases();
setDatabases(response.data);
@@ -28,6 +38,9 @@ const Sidebar: React.FC = () => {
setLoading(false);
}
};
// Fetch databases on mount
useEffect(() => {
fetchDatabases();
}, []);
@@ -86,12 +99,46 @@ const Sidebar: React.FC = () => {
return tables.filter(table => table.toLowerCase().includes(tableSearch.toLowerCase()));
}, [tables, tableSearch]);
const handleCreateDatabase = async () => {
if (!newDbName.trim()) return;
setIsCreatingDb(true);
try {
await SchemaService.createDatabase(newDbName);
setNotification({
open: true,
title: 'Success',
message: `Database '${newDbName}' created successfully.`,
severity: 'success'
});
setOpenNewDbDialog(false);
setNewDbName('');
fetchDatabases();
} catch (error: any) {
setNotification({
open: true,
title: 'Error',
message: error.response?.data?.error || error.message,
severity: 'error'
});
} finally {
setIsCreatingDb(false);
}
};
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', gap: 1.5 }}>
<Box sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Storage color="primary" sx={{ fontSize: 24 }} />
<Typography variant="h6" sx={{ fontWeight: 800, fontSize: '1.1rem' }}>Explorer</Typography>
</Box>
<Tooltip title="New Database">
<IconButton size="small" onClick={() => setOpenNewDbDialog(true)} sx={{ color: 'primary.main' }}>
<Add fontSize="small" />
</IconButton>
</Tooltip>
</Box>
<Box sx={{ px: 2, pb: 1 }}>
<TextField
@@ -229,6 +276,55 @@ const Sidebar: React.FC = () => {
</Typography>
</Stack>
</Box>
{/* New Database Dialog */}
<Dialog open={openNewDbDialog} onClose={() => setOpenNewDbDialog(false)} maxWidth="xs" fullWidth>
<DialogTitle sx={{ fontWeight: 800 }}>Create New Database</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="Database Name"
type="text"
fullWidth
variant="outlined"
value={newDbName}
onChange={(e) => setNewDbName(e.target.value)}
disabled={isCreatingDb}
onKeyDown={(e) => {
if (e.key === 'Enter') handleCreateDatabase();
}}
/>
</DialogContent>
<DialogActions sx={{ p: 2, pt: 0 }}>
<Button onClick={() => setOpenNewDbDialog(false)} disabled={isCreatingDb}>Cancel</Button>
<Button
onClick={handleCreateDatabase}
variant="contained"
disabled={isCreatingDb || !newDbName.trim()}
>
{isCreatingDb ? <CircularProgress size={24} /> : 'Create'}
</Button>
</DialogActions>
</Dialog>
{/* Local Notification */}
<Snackbar
open={notification.open}
autoHideDuration={4000}
onClose={() => setNotification({ ...notification, open: false })}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
>
<Alert
severity={notification.severity}
variant="filled"
onClose={() => setNotification({ ...notification, open: false })}
sx={{ width: '100%', borderRadius: 2 }}
>
<AlertTitle sx={{ fontWeight: 700 }}>{notification.title}</AlertTitle>
{notification.message}
</Alert>
</Snackbar>
</Box>
);
};
+1
View File
@@ -24,6 +24,7 @@ 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 }),
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 } }),